mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-24 22:20:19 +00:00
Compare commits
3 Commits
bthomee/gr
...
g-ripple/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e221267af | ||
|
|
1d7669f528 | ||
|
|
30640a626f |
21
BUILD.md
21
BUILD.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_};
|
||||
|
||||
Reference in New Issue
Block a user