Compare commits

...

3 Commits

Author SHA1 Message Date
grajanna
9e221267af fix: Default to jemalloc and skip glibc trimming
Enable jemalloc by default in Conan and CMake and propagate it to Conan
consumers. Compile out glibc malloc trimming and instrumentation when
jemalloc is enabled.

Keep GNU malloc with trimming available as an opt-out and retain the
system allocator for sanitizer configurations. Update allocator tests
and document both build choices.
2026-09-24 21:59:17 +00:00
Bart
1d7669f528 refactor: Unify upperBound and lowerBound into boundHelper (#7943)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
Co-authored-by: vlntb <13349202+vlntb@users.noreply.github.com>
2026-09-24 12:48:25 +00:00
yinyiqian1
30640a626f test: Clean up ConfidentialTransfer test helpers (#8265) 2026-09-23 16:24:41 +00:00
12 changed files with 814 additions and 644 deletions

View File

@@ -304,6 +304,7 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details.
| ---------------- | ------------- | ----------------------------------------------------------------------------- |
| `assert` | OFF | Force enabling assertions. |
| `coverage` | OFF | Prepare the coverage report. |
| `jemalloc` | ON | Use jemalloc instead of the system allocator; disables glibc malloc trimming. |
| `rust` | OFF | Build the Rust crates and the C++ code that depends on them. |
| `tests` | OFF | Build tests. |
| `unity` | OFF | Configure a unity build. |
@@ -317,6 +318,26 @@ memory) since they concatenate sources into fewer translation units. Non-unity
builds may be faster for incremental builds, and can be helpful for detecting
`#include` omissions.
### Memory allocator
Conan and CMake use jemalloc by default. In jemalloc builds, `mallocTrim` does
not call glibc's `malloc_trim` or collect trim metrics; jemalloc manages its own
arenas and memory reclamation.
To use the system allocator instead (GNU/glibc malloc with the existing
`malloc_trim(0)` behavior on Linux/glibc), add `--options '&:jemalloc=False'`
to each `conan install` command and `-Djemalloc=OFF` to the CMake configure
command. Keep the Conan and CMake options consistent. To switch an existing
system-allocator build to jemalloc, rerun Conan with
`--options '&:jemalloc=True'` and configure CMake with `-Djemalloc=ON`, since
existing build directories may cache the old option value.
The `sanitizers` Conan profile disables jemalloc when sanitizer instrumentation
is enabled so that sanitizers can intercept the system allocator. CMake also
defaults to the system allocator when `SANITIZERS` is set; use `-Djemalloc=OFF`
if reusing a build directory previously configured with jemalloc. Platforms
other than Linux/glibc do not support malloc trimming, regardless of this option.
### Rust crates
The Rust crates in `crates/` are only part of the build when `rust` is ON. With

View File

@@ -131,7 +131,17 @@ else()
set(use_lld OFF CACHE BOOL "try lld linker, clang only" FORCE)
endif()
option(jemalloc "Enables jemalloc for heap profiling" OFF)
# Sanitizers need to intercept the system allocator.
if(SANITIZERS_ENABLED)
set(JEMALLOC_DEFAULT OFF)
else()
set(JEMALLOC_DEFAULT ON)
endif()
option(
jemalloc
"Use jemalloc instead of the system allocator"
${JEMALLOC_DEFAULT}
)
option(werr "treat warnings as errors" OFF)
option(
local_protobuf

View File

@@ -98,6 +98,8 @@ tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags"
&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{ sanitizers }}", "SANITIZERS_COMPILER_FLAGS": "{{ sanitizer_compiler_flags | join(' ') }}", "SANITIZERS_LINKER_FLAGS": "{{ sanitizer_linker_flags | join(' ') }}"}
[options]
# Let sanitizers intercept the system allocator rather than jemalloc.
&:jemalloc=False
{% if enable_asan %}
# Build Boost.Context with ucontext backend (not fcontext) so that
# ASAN fiber-switching annotations (__sanitizer_start/finish_switch_fiber)

View File

@@ -52,7 +52,7 @@ class Xrpl(ConanFile):
"benchmark": True,
"coverage": False,
"fPIC": True,
"jemalloc": False,
"jemalloc": True,
"rocksdb": True,
"shared": False,
"static": True,
@@ -228,5 +228,7 @@ class Xrpl(ConanFile):
"xxhash::xxhash",
"zlib::zlib",
]
if self.options.jemalloc:
libxrpl.requires.append("jemalloc::jemalloc")
if self.options.rocksdb:
libxrpl.requires.append("rocksdb::librocksdb")

View File

@@ -13,11 +13,11 @@ namespace xrpl {
// -----------------------------------------------------------------------------
// Allocator interaction note:
// - This facility invokes glibc's malloc_trim(0) on Linux/glibc to request that
// ptmalloc return free heap pages to the OS.
// - If an alternative allocator (e.g. jemalloc or tcmalloc) is linked or
// preloaded (LD_PRELOAD), calling glibc's malloc_trim typically has no effect
// on the *active* heap. The call is harmless but may not reclaim memory
// because those allocators manage their own arenas.
// ptmalloc return free heap pages to the OS, unless built with jemalloc.
// - Builds with jemalloc disable trimming and its instrumentation entirely.
// - Other linked or preloaded allocators (LD_PRELOAD) are not detected. Calling
// glibc's malloc_trim typically has no effect on their heaps because those
// allocators manage their own arenas.
// - Only glibc sbrk/arena space is eligible for trimming; large mmap-backed
// allocations are usually returned to the OS on free regardless of trimming.
// - Call at known reclamation points (e.g., after cache sweeps / online delete)
@@ -47,18 +47,16 @@ struct MallocTrimReport
* @brief Attempt to return freed memory to the operating system.
*
* On Linux with glibc malloc, this issues ::malloc_trim(0), which may release
* free space from ptmalloc arenas back to the kernel. On other platforms, or if
* a different allocator is in use, this function is a no-op and the report will
* indicate that trimming is unsupported or had no effect.
* free space from ptmalloc arenas back to the kernel. On other platforms, or
* when built with jemalloc, this function performs no trimming or instrumentation
* and returns a default report with supported=false.
*
* @param tag Identifier for logging/debugging purposes.
* @param journal Journal for diagnostic logging.
* @return Report containing before/after metrics and the trim result.
*
* @note If an alternative allocator (jemalloc/tcmalloc) is linked or preloaded,
* calling glibc's malloc_trim may have no effect on the active heap. The
* call is harmless but typically does not reclaim memory under those
* allocators.
* @note Other linked or preloaded allocators are not detected at runtime.
* Calling glibc's malloc_trim may have no effect on their heaps.
*
* @note Only memory served from glibc's sbrk/arena heaps is eligible for trim.
* Large allocations satisfied via mmap are usually returned on free

View File

@@ -592,6 +592,20 @@ private:
SHAMapLeafNode*
belowHelper(NodePathStack& stack, BelowDirection direction) const;
/**
* Returns the nearest item strictly past `id`, in the given direction.
*
* Walks back up the path to `id`. At each inner node the branches beyond the one `id` takes
* hold the candidates, so the first non-empty one is the closest and the extreme leaf below
* it is the answer.
*
* @param id The key to search from, which need not be in the map.
* @param direction First to search upwards from `id`, Last to search downwards.
* @return An iterator to the item found, or end() if no item lies on that side of `id`.
*/
ConstIterator
boundHelper(uint256 const& id, BelowDirection direction) const;
// Simple descent
// Get a child of the specified node
SHAMapTreeNode*

View File

@@ -7,7 +7,8 @@
#include <string_view>
#if defined(__GLIBC__) && BOOST_OS_LINUX
// jemalloc manages its own arenas; glibc trimming and metrics do not apply.
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
#include <sys/resource.h>
#include <malloc.h>
@@ -43,7 +44,7 @@ namespace detail {
// cSpell:ignore statm
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
inline int
mallocTrimWithPad(std::size_t padBytes)
@@ -69,7 +70,7 @@ parseStatmRSSkB(std::string const& statm)
return (resident * pageSize) / 1024;
}
#endif // __GLIBC__ && BOOST_OS_LINUX
#endif // __GLIBC__ && BOOST_OS_LINUX && !PROFILE_JEMALLOC
} // namespace detail
@@ -80,8 +81,9 @@ mallocTrim(std::string_view tag, beast::Journal journal)
MallocTrimReport report;
#if !(defined(__GLIBC__) && BOOST_OS_LINUX)
JLOG(journal.debug()) << "malloc_trim not supported on this platform (tag=" << tag << ")";
#if !(defined(__GLIBC__) && BOOST_OS_LINUX) || defined(PROFILE_JEMALLOC)
JLOG(journal.debug()) << "malloc_trim not supported by this build's platform or allocator (tag="
<< tag << ")";
#else
// Keep glibc malloc_trim padding at 0 (default): 12h Mainnet tests across 0/256KB/1MB/16MB
// showed no clear, consistent benefit from custom padding—0 provided the best overall balance

View File

@@ -575,8 +575,10 @@ SHAMap::peekItem(uint256 const& id, SHAMapHash& hash) const
}
SHAMap::ConstIterator
SHAMap::upperBound(uint256 const& id) const
SHAMap::boundHelper(uint256 const& id, BelowDirection direction) const
{
auto const searchingForward = direction == BelowDirection::First;
NodePathStack stack;
walkTowardsKey(id, &stack);
while (!stack.empty())
@@ -584,63 +586,45 @@ SHAMap::upperBound(uint256 const& id) const
auto const [node, nodeID] = stack.top();
if (node->isLeaf())
{
auto leaf = safeDowncast<SHAMapLeafNode*>(node.get());
if (leaf->peekItem()->key() > id)
return ConstIterator(this, leaf->peekItem().get(), std::move(stack));
auto const& item = safeDowncast<SHAMapLeafNode const&>(*node).peekItem();
if (searchingForward ? (item->key() > id) : (item->key() < id))
return ConstIterator(this, item.get(), std::move(stack));
}
else
{
auto& inner = safeDowncast<SHAMapInnerNode&>(*node);
for (auto branch = selectBranch(nodeID, id) + 1; branch < kBranchFactor; ++branch)
auto const taken = selectBranch(nodeID, id);
auto const remaining = searchingForward ? (kBranchFactor - 1u - taken) : taken;
for (auto scanned = 0u; scanned < remaining; ++scanned)
{
if (!inner.isEmptyBranch(branch))
{
stack.pushChild(descendThrow(inner, branch), branch);
auto leaf = belowHelper(stack, BelowDirection::First);
if (leaf == nullptr)
Throw<SHAMapMissingNode>(type_, id);
return ConstIterator(this, leaf->peekItem().get(), std::move(stack));
}
auto const branch =
searchingForward ? (taken + 1u + scanned) : (taken - 1u - scanned);
if (inner.isEmptyBranch(branch))
continue;
stack.pushChild(descendThrow(inner, branch), branch);
auto const leaf = belowHelper(stack, direction);
if (leaf == nullptr)
Throw<SHAMapMissingNode>(type_, id);
return ConstIterator(this, leaf->peekItem().get(), std::move(stack));
}
}
stack.pop();
}
return end();
}
SHAMap::ConstIterator
SHAMap::upperBound(uint256 const& id) const
{
return boundHelper(id, BelowDirection::First);
}
SHAMap::ConstIterator
SHAMap::lowerBound(uint256 const& id) const
{
NodePathStack stack;
walkTowardsKey(id, &stack);
while (!stack.empty())
{
auto const [node, nodeID] = stack.top();
if (node->isLeaf())
{
auto leaf = safeDowncast<SHAMapLeafNode*>(node.get());
if (leaf->peekItem()->key() < id)
return ConstIterator(this, leaf->peekItem().get(), std::move(stack));
}
else
{
auto& inner = safeDowncast<SHAMapInnerNode&>(*node);
for (auto branch = selectBranch(nodeID, id); branch > 0u;)
{
--branch;
if (!inner.isEmptyBranch(branch))
{
stack.pushChild(descendThrow(inner, branch), branch);
auto leaf = belowHelper(stack, BelowDirection::Last);
if (leaf == nullptr)
Throw<SHAMapMissingNode>(type_, id);
return ConstIterator(this, leaf->peekItem().get(), std::move(stack));
}
}
}
stack.pop();
}
// TODO: what to return here?
return end();
return boundHelper(id, BelowDirection::Last);
}
bool

View File

@@ -3,28 +3,19 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/mpt.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <utility/mpt_utility.h>
#include <secp256k1.h>
#include <secp256k1_mpt.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <functional>
#include <optional>
#include <stdexcept>
@@ -54,237 +45,66 @@ protected:
return *value;
}
// Offset where the bulletproof begins in a send proof blob.
// Proof layout: [compact_sigma | bulletproof]
static constexpr size_t kBulletproofOffset = kEcSendProofLength - kEcDoubleBulletproofLength;
// Generate a forged aggregated bulletproof (double bulletproof) for
// the given values and blinding factors. Used to test that splicing
// a bulletproof claiming a different remaining balance is rejected.
// secp256k1 convention: returns 1 on success, 0 on failure.
static Buffer
getForgedBulletproof(
std::array<uint64_t, 2> const& values,
std::array<Buffer, 2> const& blindingFactors,
uint256 const& contextHash)
// Creates the MPT issuance on the given Env, authorizes and funds each
// holder, generates keys for the issuer, holders and optional auditor,
// registers the issuer/auditor keys, and converts part of each holder's
// balance to a confidential balance.
struct ConfidentialEnv
{
auto* const ctx = mpt_secp256k1_context();
// Per-holder configuration: the account, how much MPT to fund it
// with, and how much of that to convert to a confidential balance.
struct HolderInit
{
test::jtx::Account account;
std::uint64_t payAmount = 1000;
std::uint64_t convertAmount = 100;
};
secp256k1_pubkey h;
secp256k1_mpt_get_h_generator(ctx, &h);
test::jtx::MPTTester mpt;
Buffer proof(kEcDoubleBulletproofLength);
size_t proofLen = kEcDoubleBulletproofLength;
ConfidentialEnv(
test::jtx::Env& env,
test::jtx::Account const& issuer,
std::vector<HolderInit> const& holders,
std::uint32_t flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
std::optional<test::jtx::Account> auditor = std::nullopt);
unsigned char blindings[64];
std::memcpy(blindings, blindingFactors[0].data(), 32);
std::memcpy(blindings + 32, blindingFactors[1].data(), 32);
private:
static std::vector<test::jtx::Account>
extractAccounts(std::vector<HolderInit> const& holders);
};
if (secp256k1_bulletproof_prove_agg(
ctx,
proof.data(),
&proofLen,
values.data(),
blindings,
2,
&h,
contextHash.data()) == 0)
Throw<std::runtime_error>("Failed to generate forged bulletproof");
return proof;
}
// Generate a forged single bulletproof for a single value and blinding factor.
// Used to test ConvertBack overdraft prevention via bulletproof verification.
static Buffer
getForgedSingleBulletproof(
uint64_t value,
Buffer const& blindingFactor,
uint256 const& contextHash)
{
auto* const ctx = mpt_secp256k1_context();
secp256k1_pubkey h;
secp256k1_mpt_get_h_generator(ctx, &h);
Buffer proof(kEcSingleBulletproofLength);
size_t proofLen = kEcSingleBulletproofLength;
if (secp256k1_bulletproof_prove_agg(
ctx,
proof.data(),
&proofLen,
&value,
blindingFactor.data(),
1, // m = 1 (single bulletproof)
&h,
contextHash.data()) == 0)
Throw<std::runtime_error>("Failed to generate forged single bulletproof");
return proof;
}
// Forges a ConvertBack proof (compact sigma + single bulletproof) whose
// sigma component claims claimedBalance (which may be wrong) while binding
// to the real pedersen commitment and encrypted spending balance
// ciphertext already on the ledger. The bulletproof component is built
// from realBalance so it stays honest.
// mpt_get_convert_back_proof does not allow to build a proof whose amount
// exceeds the holder's claimed balance.
static Buffer
getForgedConvertBackProof(
// Create an issuance that can hold confidential balances, with the listed
// holders funded and authorized, and a key pair generated for the issuer,
// every holder, and every extra key owner. The keys are
// generated but not registered.
static void
setupConfidentialIssuance(
test::jtx::MPTTester& mpt,
test::jtx::Account const& holder,
uint64_t claimedBalance,
uint64_t realBalance,
uint64_t amt,
Buffer const& pedersenCommitment,
Buffer const& encryptedSpendingBalance,
Buffer const& pcBlindingFactor,
uint256 const& contextHash)
{
if (pedersenCommitment.size() != kCompressedEcPointLength)
Throw<std::runtime_error>("getForgedConvertBackProof: bad pedersenCommitment length");
if (encryptedSpendingBalance.size() != kEcGamalEncryptedTotalLength)
{
Throw<std::runtime_error>(
"getForgedConvertBackProof: bad encryptedSpendingBalance length");
}
if (amt > realBalance)
Throw<std::runtime_error>("getForgedConvertBackProof: amt exceeds realBalance");
test::jtx::Account const& issuer,
std::vector<test::jtx::Account> const& holders,
std::vector<test::jtx::Account> const& keyOwners = {},
std::uint32_t flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance);
auto* const ctx = mpt_secp256k1_context();
auto const holderPubKey = requireOptional(mpt.getPubKey(holder), "Missing holder pubkey");
auto const holderPrivKey =
requireOptional(mpt.getPrivKey(holder), "Missing holder privkey");
secp256k1_pubkey pkHolder;
if (secp256k1_ec_pubkey_parse(
ctx, &pkHolder, holderPubKey.data(), kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse holder's public key");
secp256k1_pubkey pcB;
if (secp256k1_ec_pubkey_parse(
ctx, &pcB, pedersenCommitment.data(), kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse pedersen commitment");
secp256k1_pubkey b1, b2;
if (secp256k1_ec_pubkey_parse(
ctx, &b1, encryptedSpendingBalance.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx,
&b2,
encryptedSpendingBalance.data() + kCompressedEcPointLength,
kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse balance ciphertext");
Buffer sigmaProof(SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE);
if (secp256k1_compact_convertback_prove(
ctx,
sigmaProof.data(),
claimedBalance,
holderPrivKey.data(),
pcBlindingFactor.data(),
&pkHolder,
&b1,
&b2,
&pcB,
contextHash.data()) != 1)
Throw<std::runtime_error>("Failed to generate convertback sigma proof");
auto const forgedBulletproof =
getForgedSingleBulletproof(realBalance - amt, pcBlindingFactor, contextHash);
Buffer proof(kEcConvertBackProofLength);
std::memcpy(proof.data(), sigmaProof.data(), SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE);
std::memcpy(
proof.data() + SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE,
forgedBulletproof.data(),
kEcSingleBulletproofLength);
return proof;
}
// Get a bad ciphertext with valid structure but cryptographic invalid for
// testing purposes. For preflight test purposes.
static Buffer const&
getBadCiphertext()
{
static Buffer const kBadCiphertext = []() {
Buffer buf(kEcGamalEncryptedTotalLength);
std::memset(buf.data(), 0xFF, kEcGamalEncryptedTotalLength);
buf.data()[0] = kEcCompressedPrefixEvenY;
buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
return buf;
}();
return kBadCiphertext;
}
// Get a trivial buffer that is structurally and mathematically valid, but
// contains invalid data that does not match the ledger state. For preclaim
// test purposes.
static Buffer const&
getTrivialCiphertext()
{
static Buffer const kTrivialCiphertext = []() {
Buffer buf(kEcGamalEncryptedTotalLength);
std::memset(buf.data(), 0, kEcGamalEncryptedTotalLength);
buf.data()[0] = kEcCompressedPrefixEvenY;
buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
buf.data()[kEcCiphertextComponentLength - 1] = 0x01;
buf.data()[kEcGamalEncryptedTotalLength - 1] = 0x01;
return buf;
}();
return kTrivialCiphertext;
}
// Returns a valid compressed EC point (33 bytes) that can pass preflight
// validation but contains invalid data for preclaim test purposes.
static Buffer const&
getTrivialCommitment()
{
static Buffer const kTrivialCommitment = []() {
Buffer buf(kEcPedersenCommitmentLength);
std::memset(buf.data(), 0, kEcPedersenCommitmentLength);
buf.data()[0] = kEcCompressedPrefixEvenY;
// Set last byte to make it a valid x-coordinate on the curve
buf.data()[kEcPedersenCommitmentLength - 1] = 0x01;
return buf;
}();
return kTrivialCommitment;
}
static std::string
getTrivialSendProofHex()
{
Buffer buf(kEcSendProofLength);
std::memset(buf.data(), 0, kEcSendProofLength);
for (std::size_t i = 0; i < kEcSendProofLength; i += kEcCiphertextComponentLength)
{
buf.data()[i] = kEcCompressedPrefixEvenY;
if (i + kEcCiphertextComponentLength - 1 < kEcSendProofLength)
buf.data()[i + kEcCiphertextComponentLength - 1] = 0x01;
}
return strHex(buf);
}
// Set up an MPT environment suitable for batch testing.
// alice is issuer; bob has 'bobAmt' in confidential spending; carol has
// 'carolAmt' in confidential spending; dave is initialised with pubkey but
// zero spending/inbox.
static void
setupBatchEnv(
test::jtx::MPTTester& mpt,
test::jtx::Account const& alice,
test::jtx::Account const& bob,
test::jtx::Account const& carol,
test::jtx::Account const& dave,
std::uint64_t bobAmt,
std::uint64_t carolAmt);
// Helper struct to encapsulate common setup for integration tests.
struct ConfidentialSendSetup
{
// Constants
uint64_t sendAmount;
size_t nRecipients;
uint32_t version;
// Blinding factors
@@ -324,55 +144,7 @@ protected:
test::jtx::Account const& dest,
test::jtx::Account const& issuer,
uint64_t amount,
std::optional<std::reference_wrapper<test::jtx::Account const>> auditor = std::nullopt)
: sendAmount(amount)
, nRecipients(auditor ? 4 : 3)
, version(mpt.getMPTokenVersion(sender))
, blindingFactor(generateBlindingFactor())
, amountBlindingFactor(blindingFactor)
, balanceBlindingFactor(generateBlindingFactor())
, senderAmt(mpt.encryptAmount(sender, amount, blindingFactor))
, destAmt(mpt.encryptAmount(dest, amount, blindingFactor))
, issuerAmt(mpt.encryptAmount(issuer, amount, blindingFactor))
, auditorAmt(
auditor ? std::optional<Buffer>(
mpt.encryptAmount(auditor->get(), amount, blindingFactor))
: std::nullopt)
, amountCommitment(mpt.getPedersenCommitment(amount, amountBlindingFactor))
, senderPubKey(requireOptional(mpt.getPubKey(sender), "Missing sender public key"))
, destPubKey(requireOptional(mpt.getPubKey(dest), "Missing destination public key"))
, issuerPubKey(requireOptional(mpt.getPubKey(issuer), "Missing issuer public key"))
, auditorPubKey(auditor ? mpt.getPubKey(auditor->get()) : std::nullopt)
, prevSpending(requireOptional(
mpt.getDecryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending),
"Missing sender spending balance"))
, prevEncryptedSpending(requireOptional(
mpt.getEncryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending),
"Missing sender encrypted spending balance"))
, balanceCommitment(mpt.getPedersenCommitment(prevSpending, balanceBlindingFactor))
{
recipients.push_back({
.publicKey = Slice(senderPubKey),
.encryptedAmount = senderAmt,
});
recipients.push_back({
.publicKey = Slice(destPubKey),
.encryptedAmount = destAmt,
});
recipients.push_back({
.publicKey = Slice(issuerPubKey),
.encryptedAmount = issuerAmt,
});
if (auditor)
{
recipients.push_back({
.publicKey =
Slice(requireOptionalRef(auditorPubKey, "Missing auditor public key")),
.encryptedAmount =
requireOptionalRef(auditorAmt, "Missing auditor encrypted amount"),
});
}
}
std::optional<std::reference_wrapper<test::jtx::Account const>> auditor = std::nullopt);
// Generate proof with current account sequence
std::optional<Buffer>
@@ -380,54 +152,78 @@ protected:
test::jtx::MPTTester& mpt,
test::jtx::Env& env,
test::jtx::Account const& sender,
test::jtx::Account const& dest) const
{
auto const ctxHash = getSendContextHash(
sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), version);
return mpt.getConfidentialSendProof(
sender,
sendAmount,
recipients,
blindingFactor,
ctxHash,
{
.pedersenCommitment = amountCommitment,
.amt = sendAmount,
.encryptedAmt = senderAmt,
.blindingFactor = amountBlindingFactor,
},
{
.pedersenCommitment = balanceCommitment,
.amt = prevSpending,
.encryptedAmt = prevEncryptedSpending,
.blindingFactor = balanceBlindingFactor,
});
}
test::jtx::Account const& dest) const;
[[nodiscard]] test::jtx::MPTConfidentialSend
sendArgs(
test::jtx::Account const& sender,
test::jtx::Account const& dest,
Buffer const& proof,
std::optional<TER> err = std::nullopt) const
{
return {
.account = sender,
.dest = dest,
.amt = sendAmount,
.proof = strHex(proof),
.senderEncryptedAmt = senderAmt,
.destEncryptedAmt = destAmt,
.issuerEncryptedAmt = issuerAmt,
.auditorEncryptedAmt = auditorAmt,
.amountCommitment = amountCommitment,
.balanceCommitment = balanceCommitment,
.err = err,
};
}
std::optional<TER> err = std::nullopt) const;
};
// Get a bad ciphertext with valid structure but cryptographic invalid for
// testing purposes. For preflight test purposes.
static Buffer const&
getBadCiphertext();
// Get a trivial buffer that is structurally and mathematically valid, but
// contains invalid data that does not match the ledger state. For preclaim
// test purposes.
static Buffer const&
getTrivialCiphertext();
// Returns a valid compressed EC point (33 bytes) that can pass preflight
// validation but contains invalid data for preclaim test purposes.
static Buffer const&
getTrivialCommitment();
// Returns a hex-encoded send proof of the correct length filled with
// placeholder data. It passes the proof length check in preflight but
// fails proof verification.
static std::string
getTrivialSendProofHex();
// Offset where the bulletproof begins in a send proof blob.
// Proof layout: [compact_sigma | bulletproof]
static constexpr size_t kBulletproofOffset = kEcSendProofLength - kEcDoubleBulletproofLength;
// Generate a forged aggregated bulletproof (double bulletproof) for
// the given values and blinding factors. Used to test that splicing
// a bulletproof claiming a different remaining balance is rejected.
static Buffer
getForgedBulletproof(
std::array<uint64_t, 2> const& values,
std::array<Buffer, 2> const& blindingFactors,
uint256 const& contextHash);
// Generate a forged single bulletproof for a single value and blinding factor.
// Used to test ConvertBack overdraft prevention via bulletproof verification.
static Buffer
getForgedSingleBulletproof(
uint64_t value,
Buffer const& blindingFactor,
uint256 const& contextHash);
// Forges a ConvertBack proof (compact sigma + single bulletproof) whose
// sigma component claims claimedBalance (which may be wrong) while binding
// to the real pedersen commitment and to the encrypted spending balance
// already on the ledger. The bulletproof component is built from the real
// remaining balance (realBalance - amt) so it stays honest.
// mpt_get_convert_back_proof validates its inputs before proving, so it
// cannot be used to build such an inconsistent proof.
static Buffer
getForgedConvertBackProof(
test::jtx::MPTTester& mpt,
test::jtx::Account const& holder,
uint64_t claimedBalance,
uint64_t realBalance,
uint64_t amt,
Buffer const& pedersenCommitment,
Buffer const& encryptedSpendingBalance,
Buffer const& pcBlindingFactor,
uint256 const& contextHash);
// Forges a ConfidentialMPTSend proof (compact sigma + double bulletproof)
// for setup.sendAmount against setup's real balance commitment/ciphertext.
// mpt_get_confidential_send_proof does not allow to build a proof whose amount
@@ -438,265 +234,7 @@ protected:
test::jtx::Env& env,
test::jtx::Account const& sender,
test::jtx::Account const& dest,
ConfidentialSendSetup const& setup)
{
auto* const ctx = mpt_secp256k1_context();
secp256k1_pubkey c1;
std::vector<secp256k1_pubkey> c2Vec(setup.recipients.size());
std::vector<secp256k1_pubkey> pkVec(setup.recipients.size());
for (std::size_t i = 0; i < setup.recipients.size(); ++i)
{
auto const& r = setup.recipients[i];
if (i == 0 &&
secp256k1_ec_pubkey_parse(
ctx, &c1, r.encryptedAmount.data(), kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse C1");
if (secp256k1_ec_pubkey_parse(
ctx,
&c2Vec[i],
r.encryptedAmount.data() + kCompressedEcPointLength,
kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse C2");
if (secp256k1_ec_pubkey_parse(
ctx, &pkVec[i], r.publicKey.data(), kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse recipient pubkey");
}
secp256k1_pubkey pkSender, pcAmount, pcBalance, b1, b2;
if (secp256k1_ec_pubkey_parse(
ctx, &pkSender, setup.senderPubKey.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx, &pcAmount, setup.amountCommitment.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx, &pcBalance, setup.balanceCommitment.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx, &b1, setup.prevEncryptedSpending.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx,
&b2,
setup.prevEncryptedSpending.data() + kCompressedEcPointLength,
kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse commitments/ciphertext");
Buffer const senderPrivKey =
requireOptional(mpt.getPrivKey(sender), "Missing sender privkey");
auto const ctxHash = getSendContextHash(
sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), setup.version);
Buffer sigmaProof(SECP256K1_COMPACT_STANDARD_PROOF_SIZE);
if (secp256k1_compact_standard_prove(
ctx,
sigmaProof.data(),
setup.sendAmount,
setup.prevSpending,
setup.blindingFactor.data(),
senderPrivKey.data(),
setup.balanceBlindingFactor.data(),
setup.recipients.size(),
&c1,
c2Vec.data(),
pkVec.data(),
&pcAmount,
&pkSender,
&pcBalance,
&b1,
&b2,
ctxHash.data()) != 1)
Throw<std::runtime_error>("Failed to generate sigma proof");
// Wraps (mod 2^64) for overdrafts, unlike the ledger's own homomorphic
// commitment subtraction (mod the curve order) — that mismatch is
// exactly what makes the forged proof fail verification.
// Computed without a wrapping `uint64` subtract: Clang UBSan treats
// unsigned overflow as fatal (see incrementConfidentialVersion).
std::uint64_t const remaining = setup.sendAmount <= setup.prevSpending
? setup.prevSpending - setup.sendAmount
: ~setup.sendAmount + setup.prevSpending + 1;
Buffer negAmountBf(kEcBlindingFactorLength);
Buffer remainingBf(kEcBlindingFactorLength);
secp256k1_mpt_scalar_negate(negAmountBf.data(), setup.amountBlindingFactor.data());
secp256k1_mpt_scalar_add(
remainingBf.data(), setup.balanceBlindingFactor.data(), negAmountBf.data());
auto const forgedBulletproof = getForgedBulletproof(
{setup.sendAmount, remaining}, {setup.amountBlindingFactor, remainingBf}, ctxHash);
Buffer combinedProof(kEcSendProofLength);
std::memcpy(combinedProof.data(), sigmaProof.data(), SECP256K1_COMPACT_STANDARD_PROOF_SIZE);
std::memcpy(
combinedProof.data() + SECP256K1_COMPACT_STANDARD_PROOF_SIZE,
forgedBulletproof.data(),
kEcDoubleBulletproofLength);
return combinedProof;
}
// Helper that wraps the boilerplate setup: Env + MPT creation, funding, key
// generation, and seeding each holder with a confidential balance.
// The caller supplies the issuer and any number of holders.
struct ConfidentialEnv
{
// Per-holder configuration: the account, how much MPT to fund it
// with, and how much of that to convert to a confidential balance.
struct HolderInit
{
test::jtx::Account account;
std::uint64_t payAmount = 1000;
std::uint64_t convertAmount = 100;
};
test::jtx::MPTTester mpt;
ConfidentialEnv(
test::jtx::Env& env,
test::jtx::Account const& issuer,
std::vector<HolderInit> const& holders,
std::uint32_t flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
std::optional<test::jtx::Account> auditor = std::nullopt)
: mpt{env, issuer, {.holders = extractAccounts(holders), .auditor = auditor}}
{
mpt.create({.ownerCount = 1, .flags = flags});
for (auto const& h : holders)
{
mpt.authorize({.account = h.account});
if ((flags & tfMPTRequireAuth) != 0)
mpt.authorize({.account = issuer, .holder = h.account});
mpt.pay(issuer, h.account, h.payAmount);
}
mpt.generateKeyPair(issuer);
for (auto const& h : holders)
mpt.generateKeyPair(h.account);
if (auditor)
mpt.generateKeyPair(requireOptionalRef(auditor, "Missing auditor"));
mpt.set({
.account = issuer,
.issuerPubKey = mpt.getPubKey(issuer),
.auditorPubKey = auditor
? mpt.getPubKey(requireOptionalRef(auditor, "Missing auditor"))
: std::optional<Buffer>{},
});
for (auto const& h : holders)
{
mpt.convert({
.account = h.account,
.amt = h.convertAmount,
.holderPubKey = mpt.getPubKey(h.account),
});
mpt.mergeInbox({.account = h.account});
}
}
private:
static std::vector<test::jtx::Account>
extractAccounts(std::vector<HolderInit> const& holders)
{
std::vector<test::jtx::Account> accounts;
accounts.reserve(holders.size());
for (auto const& h : holders)
accounts.push_back(h.account);
return accounts;
}
};
// Create an issuance that can hold confidential balances, with the listed
// holders funded and authorized, and a key pair generated for the issuer,
// every holder, and every extra key owner. The keys are
// generated but not registered.
static void
setupConfidentialIssuance(
test::jtx::MPTTester& mpt,
test::jtx::Account const& issuer,
std::vector<test::jtx::Account> const& holders,
std::vector<test::jtx::Account> const& keyOwners = {},
std::uint32_t flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance);
// Set up an MPT environment suitable for batch testing.
// alice is issuer; bob has 'bobAmt' in confidential spending; carol has
// 'carolAmt' in confidential spending; dave is initialised with pubkey but
// zero spending/inbox.
static void
setupBatchEnv(
test::jtx::MPTTester& mpt,
test::jtx::Account const& alice,
test::jtx::Account const& bob,
test::jtx::Account const& carol,
test::jtx::Account const& dave,
std::uint64_t bobAmt,
std::uint64_t carolAmt)
{
using namespace test::jtx;
mpt.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
});
mpt.authorize({.account = bob});
mpt.authorize({.account = carol});
mpt.authorize({.account = dave});
if (bobAmt > 0)
mpt.pay(alice, bob, bobAmt);
if (carolAmt > 0)
mpt.pay(alice, carol, carolAmt);
mpt.generateKeyPair(alice);
mpt.generateKeyPair(bob);
mpt.generateKeyPair(carol);
mpt.generateKeyPair(dave);
mpt.set({
.account = alice,
.issuerPubKey = mpt.getPubKey(alice),
});
if (bobAmt > 0)
{
mpt.convert({
.account = bob,
.amt = bobAmt,
.holderPubKey = mpt.getPubKey(bob),
});
mpt.mergeInbox({.account = bob});
}
else
{
mpt.convert({
.account = bob,
.amt = 0,
.holderPubKey = mpt.getPubKey(bob),
});
}
if (carolAmt > 0)
{
mpt.convert({
.account = carol,
.amt = carolAmt,
.holderPubKey = mpt.getPubKey(carol),
});
mpt.mergeInbox({.account = carol});
}
else
{
mpt.convert({
.account = carol,
.amt = 0,
.holderPubKey = mpt.getPubKey(carol),
});
}
// dave: register pubkey only (0 spending/inbox)
mpt.convert({
.account = dave,
.amt = 0,
.holderPubKey = mpt.getPubKey(dave),
});
}
ConfidentialSendSetup const& setup);
};
} // namespace xrpl

View File

@@ -1,13 +1,89 @@
#include <test/jtx/ConfidentialTransfer.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/mpt.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <utility/mpt_utility.h>
#include <secp256k1.h>
#include <secp256k1_mpt.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <functional>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
namespace xrpl {
ConfidentialTransferTestBase::ConfidentialEnv::ConfidentialEnv(
test::jtx::Env& env,
test::jtx::Account const& issuer,
std::vector<HolderInit> const& holders,
std::uint32_t flags,
std::optional<test::jtx::Account> auditor)
: mpt{env, issuer, {.holders = extractAccounts(holders), .auditor = auditor}}
{
mpt.create({.ownerCount = 1, .flags = flags});
for (auto const& h : holders)
{
mpt.authorize({.account = h.account});
if ((flags & tfMPTRequireAuth) != 0)
mpt.authorize({.account = issuer, .holder = h.account});
mpt.pay(issuer, h.account, h.payAmount);
}
mpt.generateKeyPair(issuer);
for (auto const& h : holders)
mpt.generateKeyPair(h.account);
if (auditor)
mpt.generateKeyPair(requireOptionalRef(auditor, "Missing auditor"));
mpt.set({
.account = issuer,
.issuerPubKey = mpt.getPubKey(issuer),
.auditorPubKey = auditor ? mpt.getPubKey(requireOptionalRef(auditor, "Missing auditor"))
: std::optional<Buffer>{},
});
for (auto const& h : holders)
{
mpt.convert({
.account = h.account,
.amt = h.convertAmount,
.holderPubKey = mpt.getPubKey(h.account),
});
mpt.mergeInbox({.account = h.account});
}
}
std::vector<test::jtx::Account>
ConfidentialTransferTestBase::ConfidentialEnv::extractAccounts(
std::vector<HolderInit> const& holders)
{
std::vector<test::jtx::Account> accounts;
accounts.reserve(holders.size());
for (auto const& h : holders)
accounts.push_back(h.account);
return accounts;
}
void
ConfidentialTransferTestBase::setupConfidentialIssuance(
test::jtx::MPTTester& mpt,
@@ -34,4 +110,478 @@ ConfidentialTransferTestBase::setupConfidentialIssuance(
mpt.generateKeyPair(keyOwner);
}
void
ConfidentialTransferTestBase::setupBatchEnv(
test::jtx::MPTTester& mpt,
test::jtx::Account const& alice,
test::jtx::Account const& bob,
test::jtx::Account const& carol,
test::jtx::Account const& dave,
std::uint64_t bobAmt,
std::uint64_t carolAmt)
{
using namespace test::jtx;
mpt.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
});
mpt.authorize({.account = bob});
mpt.authorize({.account = carol});
mpt.authorize({.account = dave});
if (bobAmt > 0)
mpt.pay(alice, bob, bobAmt);
if (carolAmt > 0)
mpt.pay(alice, carol, carolAmt);
mpt.generateKeyPair(alice);
mpt.generateKeyPair(bob);
mpt.generateKeyPair(carol);
mpt.generateKeyPair(dave);
mpt.set({
.account = alice,
.issuerPubKey = mpt.getPubKey(alice),
});
if (bobAmt > 0)
{
mpt.convert({
.account = bob,
.amt = bobAmt,
.holderPubKey = mpt.getPubKey(bob),
});
mpt.mergeInbox({.account = bob});
}
else
{
mpt.convert({
.account = bob,
.amt = 0,
.holderPubKey = mpt.getPubKey(bob),
});
}
if (carolAmt > 0)
{
mpt.convert({
.account = carol,
.amt = carolAmt,
.holderPubKey = mpt.getPubKey(carol),
});
mpt.mergeInbox({.account = carol});
}
else
{
mpt.convert({
.account = carol,
.amt = 0,
.holderPubKey = mpt.getPubKey(carol),
});
}
// dave: register pubkey only (0 spending/inbox)
mpt.convert({
.account = dave,
.amt = 0,
.holderPubKey = mpt.getPubKey(dave),
});
}
ConfidentialTransferTestBase::ConfidentialSendSetup::ConfidentialSendSetup(
test::jtx::MPTTester& mpt,
test::jtx::Account const& sender,
test::jtx::Account const& dest,
test::jtx::Account const& issuer,
uint64_t amount,
std::optional<std::reference_wrapper<test::jtx::Account const>> auditor)
: sendAmount(amount)
, version(mpt.getMPTokenVersion(sender))
, blindingFactor(generateBlindingFactor())
, amountBlindingFactor(blindingFactor)
, balanceBlindingFactor(generateBlindingFactor())
, senderAmt(mpt.encryptAmount(sender, amount, blindingFactor))
, destAmt(mpt.encryptAmount(dest, amount, blindingFactor))
, issuerAmt(mpt.encryptAmount(issuer, amount, blindingFactor))
, auditorAmt(
auditor ? std::optional<Buffer>(mpt.encryptAmount(auditor->get(), amount, blindingFactor))
: std::nullopt)
, amountCommitment(mpt.getPedersenCommitment(amount, amountBlindingFactor))
, senderPubKey(requireOptional(mpt.getPubKey(sender), "Missing sender public key"))
, destPubKey(requireOptional(mpt.getPubKey(dest), "Missing destination public key"))
, issuerPubKey(requireOptional(mpt.getPubKey(issuer), "Missing issuer public key"))
, auditorPubKey(auditor ? mpt.getPubKey(auditor->get()) : std::nullopt)
, prevSpending(requireOptional(
mpt.getDecryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending),
"Missing sender spending balance"))
, prevEncryptedSpending(requireOptional(
mpt.getEncryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending),
"Missing sender encrypted spending balance"))
, balanceCommitment(mpt.getPedersenCommitment(prevSpending, balanceBlindingFactor))
{
recipients.push_back({
.publicKey = Slice(senderPubKey),
.encryptedAmount = senderAmt,
});
recipients.push_back({
.publicKey = Slice(destPubKey),
.encryptedAmount = destAmt,
});
recipients.push_back({
.publicKey = Slice(issuerPubKey),
.encryptedAmount = issuerAmt,
});
if (auditor)
{
recipients.push_back({
.publicKey = Slice(requireOptionalRef(auditorPubKey, "Missing auditor public key")),
.encryptedAmount = requireOptionalRef(auditorAmt, "Missing auditor encrypted amount"),
});
}
}
std::optional<Buffer>
ConfidentialTransferTestBase::ConfidentialSendSetup::generateProof(
test::jtx::MPTTester& mpt,
test::jtx::Env& env,
test::jtx::Account const& sender,
test::jtx::Account const& dest) const
{
auto const ctxHash =
getSendContextHash(sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), version);
return mpt.getConfidentialSendProof(
sender,
sendAmount,
recipients,
blindingFactor,
ctxHash,
{
.pedersenCommitment = amountCommitment,
.amt = sendAmount,
.encryptedAmt = senderAmt,
.blindingFactor = amountBlindingFactor,
},
{
.pedersenCommitment = balanceCommitment,
.amt = prevSpending,
.encryptedAmt = prevEncryptedSpending,
.blindingFactor = balanceBlindingFactor,
});
}
test::jtx::MPTConfidentialSend
ConfidentialTransferTestBase::ConfidentialSendSetup::sendArgs(
test::jtx::Account const& sender,
test::jtx::Account const& dest,
Buffer const& proof,
std::optional<TER> err) const
{
return {
.account = sender,
.dest = dest,
.amt = sendAmount,
.proof = strHex(proof),
.senderEncryptedAmt = senderAmt,
.destEncryptedAmt = destAmt,
.issuerEncryptedAmt = issuerAmt,
.auditorEncryptedAmt = auditorAmt,
.amountCommitment = amountCommitment,
.balanceCommitment = balanceCommitment,
.err = err,
};
}
Buffer const&
ConfidentialTransferTestBase::getBadCiphertext()
{
static Buffer const kBadCiphertext = []() {
Buffer buf(kEcGamalEncryptedTotalLength);
std::memset(buf.data(), 0xFF, kEcGamalEncryptedTotalLength);
buf.data()[0] = kEcCompressedPrefixEvenY;
buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
return buf;
}();
return kBadCiphertext;
}
Buffer const&
ConfidentialTransferTestBase::getTrivialCiphertext()
{
static Buffer const kTrivialCiphertext = []() {
Buffer buf(kEcGamalEncryptedTotalLength);
std::memset(buf.data(), 0, kEcGamalEncryptedTotalLength);
buf.data()[0] = kEcCompressedPrefixEvenY;
buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY;
buf.data()[kEcCiphertextComponentLength - 1] = 0x01;
buf.data()[kEcGamalEncryptedTotalLength - 1] = 0x01;
return buf;
}();
return kTrivialCiphertext;
}
Buffer const&
ConfidentialTransferTestBase::getTrivialCommitment()
{
static Buffer const kTrivialCommitment = []() {
Buffer buf(kEcPedersenCommitmentLength);
std::memset(buf.data(), 0, kEcPedersenCommitmentLength);
buf.data()[0] = kEcCompressedPrefixEvenY;
// Set last byte to make it a valid x-coordinate on the curve
buf.data()[kEcPedersenCommitmentLength - 1] = 0x01;
return buf;
}();
return kTrivialCommitment;
}
std::string
ConfidentialTransferTestBase::getTrivialSendProofHex()
{
Buffer buf(kEcSendProofLength);
std::memset(buf.data(), 0, kEcSendProofLength);
for (std::size_t i = 0; i < kEcSendProofLength; i += kEcCiphertextComponentLength)
{
buf.data()[i] = kEcCompressedPrefixEvenY;
if (i + kEcCiphertextComponentLength - 1 < kEcSendProofLength)
buf.data()[i + kEcCiphertextComponentLength - 1] = 0x01;
}
return strHex(buf);
}
Buffer
ConfidentialTransferTestBase::getForgedBulletproof(
std::array<uint64_t, 2> const& values,
std::array<Buffer, 2> const& blindingFactors,
uint256 const& contextHash)
{
auto* const ctx = mpt_secp256k1_context();
secp256k1_pubkey h;
secp256k1_mpt_get_h_generator(ctx, &h);
Buffer proof(kEcDoubleBulletproofLength);
size_t proofLen = kEcDoubleBulletproofLength;
unsigned char blindings[64];
std::memcpy(blindings, blindingFactors[0].data(), 32);
std::memcpy(blindings + 32, blindingFactors[1].data(), 32);
if (secp256k1_bulletproof_prove_agg(
ctx, proof.data(), &proofLen, values.data(), blindings, 2, &h, contextHash.data()) == 0)
Throw<std::runtime_error>("Failed to generate forged bulletproof");
return proof;
}
Buffer
ConfidentialTransferTestBase::getForgedSingleBulletproof(
uint64_t value,
Buffer const& blindingFactor,
uint256 const& contextHash)
{
auto* const ctx = mpt_secp256k1_context();
secp256k1_pubkey h;
secp256k1_mpt_get_h_generator(ctx, &h);
Buffer proof(kEcSingleBulletproofLength);
size_t proofLen = kEcSingleBulletproofLength;
if (secp256k1_bulletproof_prove_agg(
ctx,
proof.data(),
&proofLen,
&value,
blindingFactor.data(),
1, // m = 1 (single bulletproof)
&h,
contextHash.data()) == 0)
Throw<std::runtime_error>("Failed to generate forged single bulletproof");
return proof;
}
Buffer
ConfidentialTransferTestBase::getForgedConvertBackProof(
test::jtx::MPTTester& mpt,
test::jtx::Account const& holder,
uint64_t claimedBalance,
uint64_t realBalance,
uint64_t amt,
Buffer const& pedersenCommitment,
Buffer const& encryptedSpendingBalance,
Buffer const& pcBlindingFactor,
uint256 const& contextHash)
{
if (pedersenCommitment.size() != kCompressedEcPointLength)
Throw<std::runtime_error>("getForgedConvertBackProof: bad pedersenCommitment length");
if (encryptedSpendingBalance.size() != kEcGamalEncryptedTotalLength)
{
Throw<std::runtime_error>("getForgedConvertBackProof: bad encryptedSpendingBalance length");
}
if (amt > realBalance)
Throw<std::runtime_error>("getForgedConvertBackProof: amt exceeds realBalance");
auto* const ctx = mpt_secp256k1_context();
auto const holderPubKey = requireOptional(mpt.getPubKey(holder), "Missing holder pubkey");
auto const holderPrivKey = requireOptional(mpt.getPrivKey(holder), "Missing holder privkey");
secp256k1_pubkey pkHolder;
if (secp256k1_ec_pubkey_parse(ctx, &pkHolder, holderPubKey.data(), kCompressedEcPointLength) !=
1)
Throw<std::runtime_error>("Failed to parse holder's public key");
secp256k1_pubkey pcB;
if (secp256k1_ec_pubkey_parse(ctx, &pcB, pedersenCommitment.data(), kCompressedEcPointLength) !=
1)
Throw<std::runtime_error>("Failed to parse pedersen commitment");
secp256k1_pubkey b1, b2;
if (secp256k1_ec_pubkey_parse(
ctx, &b1, encryptedSpendingBalance.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx,
&b2,
encryptedSpendingBalance.data() + kCompressedEcPointLength,
kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse balance ciphertext");
Buffer sigmaProof(SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE);
if (secp256k1_compact_convertback_prove(
ctx,
sigmaProof.data(),
claimedBalance,
holderPrivKey.data(),
pcBlindingFactor.data(),
&pkHolder,
&b1,
&b2,
&pcB,
contextHash.data()) != 1)
Throw<std::runtime_error>("Failed to generate convertback sigma proof");
auto const forgedBulletproof =
getForgedSingleBulletproof(realBalance - amt, pcBlindingFactor, contextHash);
Buffer proof(kEcConvertBackProofLength);
std::memcpy(proof.data(), sigmaProof.data(), SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE);
std::memcpy(
proof.data() + SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE,
forgedBulletproof.data(),
kEcSingleBulletproofLength);
return proof;
}
Buffer
ConfidentialTransferTestBase::getForgedSendProof(
test::jtx::MPTTester& mpt,
test::jtx::Env& env,
test::jtx::Account const& sender,
test::jtx::Account const& dest,
ConfidentialSendSetup const& setup)
{
auto* const ctx = mpt_secp256k1_context();
secp256k1_pubkey c1;
std::vector<secp256k1_pubkey> c2Vec(setup.recipients.size());
std::vector<secp256k1_pubkey> pkVec(setup.recipients.size());
for (std::size_t i = 0; i < setup.recipients.size(); ++i)
{
auto const& r = setup.recipients[i];
if (i == 0 &&
secp256k1_ec_pubkey_parse(
ctx, &c1, r.encryptedAmount.data(), kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse C1");
if (secp256k1_ec_pubkey_parse(
ctx,
&c2Vec[i],
r.encryptedAmount.data() + kCompressedEcPointLength,
kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse C2");
if (secp256k1_ec_pubkey_parse(
ctx, &pkVec[i], r.publicKey.data(), kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse recipient pubkey");
}
secp256k1_pubkey pkSender, pcAmount, pcBalance, b1, b2;
if (secp256k1_ec_pubkey_parse(
ctx, &pkSender, setup.senderPubKey.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx, &pcAmount, setup.amountCommitment.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx, &pcBalance, setup.balanceCommitment.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx, &b1, setup.prevEncryptedSpending.data(), kCompressedEcPointLength) != 1 ||
secp256k1_ec_pubkey_parse(
ctx,
&b2,
setup.prevEncryptedSpending.data() + kCompressedEcPointLength,
kCompressedEcPointLength) != 1)
Throw<std::runtime_error>("Failed to parse commitments/ciphertext");
Buffer const senderPrivKey = requireOptional(mpt.getPrivKey(sender), "Missing sender privkey");
auto const ctxHash = getSendContextHash(
sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), setup.version);
Buffer sigmaProof(SECP256K1_COMPACT_STANDARD_PROOF_SIZE);
if (secp256k1_compact_standard_prove(
ctx,
sigmaProof.data(),
setup.sendAmount,
setup.prevSpending,
setup.blindingFactor.data(),
senderPrivKey.data(),
setup.balanceBlindingFactor.data(),
setup.recipients.size(),
&c1,
c2Vec.data(),
pkVec.data(),
&pcAmount,
&pkSender,
&pcBalance,
&b1,
&b2,
ctxHash.data()) != 1)
Throw<std::runtime_error>("Failed to generate sigma proof");
// Wraps (mod 2^64) for overdrafts, unlike the ledger's own homomorphic
// commitment subtraction (mod the curve order) — that mismatch is
// exactly what makes the forged proof fail verification.
// Computed without a wrapping `uint64` subtract: Clang UBSan treats
// unsigned overflow as fatal (see incrementConfidentialVersion).
std::uint64_t const remaining = setup.sendAmount <= setup.prevSpending
? setup.prevSpending - setup.sendAmount
: ~setup.sendAmount + setup.prevSpending + 1;
Buffer negAmountBf(kEcBlindingFactorLength);
Buffer remainingBf(kEcBlindingFactorLength);
secp256k1_mpt_scalar_negate(negAmountBf.data(), setup.amountBlindingFactor.data());
secp256k1_mpt_scalar_add(
remainingBf.data(), setup.balanceBlindingFactor.data(), negAmountBf.data());
auto const forgedBulletproof = getForgedBulletproof(
{setup.sendAmount, remaining}, {setup.amountBlindingFactor, remainingBf}, ctxHash);
Buffer combinedProof(kEcSendProofLength);
std::memcpy(combinedProof.data(), sigmaProof.data(), SECP256K1_COMPACT_STANDARD_PROOF_SIZE);
std::memcpy(
combinedProof.data() + SECP256K1_COMPACT_STANDARD_PROOF_SIZE,
forgedBulletproof.data(),
kEcDoubleBulletproofLength);
return combinedProof;
}
} // namespace xrpl

View File

@@ -12,7 +12,7 @@ using namespace xrpl;
// cSpell:ignore statm
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
namespace xrpl::detail {
long
parseStatmRSSkB(std::string const& statm);
@@ -48,7 +48,7 @@ TEST(MallocTrimReport, structure)
EXPECT_EQ(report.deltaKB(), 0);
}
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
TEST(ParseStatmRSSkB, standard_format)
{
using xrpl::detail::parseStatmRSSkB;
@@ -127,7 +127,7 @@ TEST(MallocTrim, without_debug_logging)
MallocTrimReport const report = mallocTrim("without_debug", journal);
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
EXPECT_EQ(report.supported, true);
EXPECT_GE(report.trimResult, 0);
EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1});
@@ -149,7 +149,7 @@ TEST(MallocTrim, empty_tag)
beast::Journal const journal{beast::Journal::getNullSink()};
MallocTrimReport const report = mallocTrim("", journal);
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
EXPECT_EQ(report.supported, true);
EXPECT_GE(report.trimResult, 0);
#else
@@ -179,7 +179,7 @@ TEST(MallocTrim, with_debug_logging)
MallocTrimReport const report = mallocTrim("debug_test", journal);
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
EXPECT_EQ(report.supported, true);
EXPECT_GE(report.trimResult, 0);
EXPECT_GE(report.durationUs.count(), 0);
@@ -188,9 +188,12 @@ TEST(MallocTrim, with_debug_logging)
#else
EXPECT_EQ(report.supported, false);
EXPECT_EQ(report.trimResult, -1);
EXPECT_EQ(report.rssBeforeKB, -1);
EXPECT_EQ(report.rssAfterKB, -1);
EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1});
EXPECT_EQ(report.minfltDelta, -1);
EXPECT_EQ(report.majfltDelta, -1);
EXPECT_EQ(report.deltaKB(), 0);
#endif
}
@@ -203,7 +206,7 @@ TEST(MallocTrim, repeated_calls)
{
MallocTrimReport const report = mallocTrim("iteration_" + std::to_string(i), journal);
#if defined(__GLIBC__) && BOOST_OS_LINUX
#if defined(__GLIBC__) && BOOST_OS_LINUX && !defined(PROFILE_JEMALLOC)
EXPECT_EQ(report.supported, true);
EXPECT_GE(report.trimResult, 0);
#else

View File

@@ -455,6 +455,52 @@ TEST_F(SHAMapTraversal, bounds_agree_with_iteration_for_absent_keys)
}
}
TEST_F(SHAMapTraversal, bounds_on_empty_map_return_end)
{
tests::TestNodeFamily f{j_};
SHAMap map{SHAMapType::FREE, f};
map.setUnbacked();
// The root is a childless inner node, so boundHelper's inner-node branch scans every branch on
// the requested side of the one id selects, finds them all empty, and falls through to end()
// rather than dereference a child.
EXPECT_EQ(map.upperBound(uint256{}), map.end());
EXPECT_EQ(map.lowerBound(uint256{}), map.end());
uint256 probe;
std::fill_n(probe.begin(), probe.size(), std::uint8_t{0xff});
EXPECT_EQ(map.upperBound(probe), map.end());
EXPECT_EQ(map.lowerBound(probe), map.end());
}
TEST_F(SHAMapTraversal, bounds_on_single_item_map_use_the_leaf_below_the_root)
{
tests::TestNodeFamily f{j_};
SHAMap map{SHAMapType::FREE, f};
auto const key = deepFanOutKeys().front();
fillMap(map, {key});
// fillMap adds items in-process, so root_ stays an inner node with the single leaf below it.
// The stack holds both, so boundHelper examines the leaf first.
uint256 below = key;
--below;
uint256 above = key;
++above;
auto const upper = map.upperBound(below);
ASSERT_NE(upper, map.end());
EXPECT_EQ(upper->key(), key);
EXPECT_EQ(map.upperBound(key), map.end());
EXPECT_EQ(map.upperBound(above), map.end());
auto const lower = map.lowerBound(above);
ASSERT_NE(lower, map.end());
EXPECT_EQ(lower->key(), key);
EXPECT_EQ(map.lowerBound(key), map.end());
EXPECT_EQ(map.lowerBound(below), map.end());
}
TEST_F(SHAMapTraversal, iteration_survives_deletions)
{
tests::TestNodeFamily f{j_};