Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion conan.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"protobuf/6.33.5#d96d52ba5baaaa532f47bda866ad87a5%1774467363.12",
"openssl/3.6.2#4789bbf131b77d0515d15e094c8f697f%1778071755.506",
"nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1775040983.408",
"mpt-crypto/0.3.0-rc4#67142060d3e9b33cf49b980b1bfba40a%1781115459.717",
"mpt-crypto/0.3.0-rc5#536cb5bdf3cc7ce1b29373fa94d7eab9%1781638154.791",
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
"libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
Expand Down Expand Up @@ -42,12 +42,18 @@
],
"python_requires": [],
"overrides": {
"openssl/[>=1.1 <4]": [
"openssl/3.6.2"
],
"protobuf/[>=5.27.0 <7]": [
"protobuf/6.33.5"
],
"lz4/1.9.4": [
"lz4/1.10.0"
],
"openssl/[>=3.5 <4]": [
"openssl/3.6.2#4789bbf131b77d0515d15e094c8f697f"
],
"boost/[>=1.83.0 <1.91.0]": [
"boost/1.91.0"
],
Expand Down
4 changes: 2 additions & 2 deletions conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ class Xrpl(ConanFile):
"ed25519/2015.03",
"grpc/1.78.1",
"libarchive/3.8.7",
"mpt-crypto/0.3.0-rc4",
"mpt-crypto/0.3.0-rc5",
"nudb/2.0.9",
"openssl/3.6.2",
"secp256k1/0.7.1",
"soci/4.0.3",
"zlib/1.3.2",
Expand Down Expand Up @@ -134,6 +133,7 @@ def requirements(self):
self.requires("boost/1.91.0", force=True, transitive_headers=True)
self.requires("date/3.0.4", transitive_headers=True)
self.requires("lz4/1.10.0", force=True)
self.requires("openssl/3.6.2", force=True)
self.requires("protobuf/6.33.5", force=True)
self.requires("sqlite3/3.53.0", force=True)
if self.options.jemalloc:
Expand Down
2 changes: 2 additions & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,10 @@ words:
- queuable
- Raphson
- replayer
- rerandomize
- rerandomization
- rerandomized
- rerandomizes
- rerere
- retriable
- RIPD
Expand Down
15 changes: 15 additions & 0 deletions include/xrpl/protocol/ConfidentialTransfer.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,21 @@ homomorphicAdd(Slice const& a, Slice const& b);
std::optional<Buffer>
homomorphicSubtract(Slice const& a, Slice const& b);

/**
* @brief Re-randomizes an ElGamal ciphertext without changing its plaintext.
*
* Adds Enc(0; randomness) under the supplied public key to the ciphertext.
* This is used when a public, deterministic scalar must perturb ciphertext
* randomness while preserving ledger reproducibility.
*
* @param ciphertext The ciphertext to re-randomize (66 bytes).
* @param pubKeySlice The ElGamal public key matching the ciphertext recipient.
* @param randomness The scalar used as zero-encryption randomness (32 bytes).
* @return The re-randomized ciphertext, or std::nullopt on failure.
*/
std::optional<Buffer>
rerandomizeCiphertext(Slice const& ciphertext, Slice const& pubKeySlice, Slice const& randomness);

/**
* @brief Encrypts an amount using ElGamal encryption.
*
Expand Down
10 changes: 10 additions & 0 deletions src/libxrpl/protocol/ConfidentialTransfer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,16 @@
return serializeEcPair(diff);
}

std::optional<Buffer>
rerandomizeCiphertext(Slice const& ciphertext, Slice const& pubKeySlice, Slice const& randomness)
{
auto zero = encryptAmount(0, pubKeySlice, randomness);
if (!zero)
return std::nullopt;

Check warning on line 228 in src/libxrpl/protocol/ConfidentialTransfer.cpp

View check run for this annotation

Codecov / codecov/patch

src/libxrpl/protocol/ConfidentialTransfer.cpp#L228

Added line #L228 was not covered by tests

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exclude line cov

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I just merged the code. Would you mind adding it in the next PR?


return homomorphicAdd(ciphertext, *zero);
}

Buffer
generateBlindingFactor()
{
Expand Down
20 changes: 17 additions & 3 deletions src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <xrpl/tx/transactors/token/ConfidentialMPTSend.h>

#include <xrpl/basics/Slice.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
Expand Down Expand Up @@ -270,10 +271,11 @@ ConfidentialMPTSend::doApply()

auto sleSenderMPToken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
auto sleDestinationMPToken = view().peek(keylet::mptoken(mptIssuanceID, destination));
auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));

auto const sleDestAcct = view().read(keylet::account(destination));

if (!sleSenderMPToken || !sleDestinationMPToken || !sleDestAcct)
if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance || !sleDestAcct)
return tecINTERNAL; // LCOV_EXCL_LINE

// Deposit preauth authorization was already verified in preclaim.
Expand All @@ -285,6 +287,8 @@ ConfidentialMPTSend::doApply()
auto const senderEc = ctx_.tx[sfSenderEncryptedAmount];
auto const destEc = ctx_.tx[sfDestinationEncryptedAmount];
auto const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
auto const proof = ctx_.tx[sfZKProof];
Slice const sendChallenge{proof.data(), kEcBlindingFactorLength};

auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];

Expand Down Expand Up @@ -321,8 +325,13 @@ ConfidentialMPTSend::doApply()

// Add to destination's inbox balance
{
auto rerandomizedDestEc = rerandomizeCiphertext(
destEc, (*sleDestinationMPToken)[sfHolderEncryptionKey], sendChallenge);
if (!rerandomizedDestEc)
return tecINTERNAL; // LCOV_EXCL_LINE

auto const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox];
auto newInbox = homomorphicAdd(curInbox, destEc);
auto newInbox = homomorphicAdd(curInbox, *rerandomizedDestEc);
if (!newInbox)
return tecINTERNAL; // LCOV_EXCL_LINE

Expand All @@ -331,8 +340,13 @@ ConfidentialMPTSend::doApply()

// Add to issuer's balance
{
auto rerandomizedIssuerEc =
rerandomizeCiphertext(issuerEc, (*sleIssuance)[sfIssuerEncryptionKey], sendChallenge);
if (!rerandomizedIssuerEc)
return tecINTERNAL; // LCOV_EXCL_LINE

auto const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance];
auto newIssuerEnc = homomorphicAdd(curIssuerEnc, issuerEc);
auto newIssuerEnc = homomorphicAdd(curIssuerEnc, *rerandomizedIssuerEc);
if (!newIssuerEnc)
return tecINTERNAL; // LCOV_EXCL_LINE

Expand Down
125 changes: 119 additions & 6 deletions src/test/app/ConfidentialTransfer_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
#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/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
Expand All @@ -28,10 +30,18 @@
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/tx/apply.h>

#include <openssl/evp.h>
#include <utility/mpt_utility.h>

#include <secp256k1.h>
#include <secp256k1_mpt.h>

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
Expand All @@ -40,13 +50,14 @@
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>

namespace xrpl {

// NOLINTBEGIN(misc-const-correctness, bugprone-unchecked-optional-access)
class ConfidentialTransferTest : public ConfidentialTransferTestBase
class ConfidentialTransfer_test : public ConfidentialTransferTestBase
{
void
testConvert(FeatureBitset features)
Expand Down Expand Up @@ -7840,6 +7851,111 @@ class ConfidentialTransferTest : public ConfidentialTransferTestBase
}
}

void
testSendRerandomizesRecipientInboxAgainstMergeCancellation(FeatureBitset features)
{
testcase("Send: recipient inbox rerandomization prevents merge cancellation");

using namespace test::jtx;
Env env{*this, features};
Account const alice("alice"), bob("bob"), carol("carol");
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanConfidentialAmount,
});

mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 100);
mptAlice.pay(alice, carol, 100);

mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(carol);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});

// Carol converts 50 and merge inbox
mptAlice.convert({
.account = carol,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(carol),
});
mptAlice.mergeInbox({.account = carol});

// Bob converts 20 and has not merged yet. His spending
// balance is the canonical zero Enc(0; r0), and his inbox uses the
// publicly revealed convert blinding factor.
Buffer const convertBlindingFactor = generateBlindingFactor();
mptAlice.convert({
.account = bob,
.amt = 20,
.holderPubKey = mptAlice.getPubKey(bob),
.blindingFactor = convertBlindingFactor,
});

// Derive the deterministic canonical-zero randomness r0 used for
// Bob's first spending balance.
auto getCanonicalZeroBlindingFactor = [](AccountID const& account, MPTID const& mptID) {
Buffer scalar(kEcBlindingFactorLength);
std::array<unsigned char, 51> hashInput{};
std::memcpy(hashInput.data(), "EncZero", 7);
std::memcpy(hashInput.data() + 7, account.data(), account.size());
std::memcpy(hashInput.data() + 27, mptID.data(), mptID.size());

for (;;)
{
unsigned int mdLen = kEcBlindingFactorLength;
if (EVP_Digest(
hashInput.data(),
hashInput.size(),
scalar.data(),
&mdLen,
EVP_sha256(),
nullptr) != 1)
{
Throw<std::runtime_error>("Failed to derive canonical zero blinding factor");
}

if (secp256k1_ec_seckey_verify(mpt_secp256k1_context(), scalar.data()))
return scalar;

std::memcpy(hashInput.data(), scalar.data(), scalar.size());
}
};

// Pick randomness that would cancel Bob's MergeInbox C1 to infinity
// without receiver-side re-randomization.
auto negateScalarSum = [](Buffer const& lhs, Buffer const& rhs) {
Buffer sum(kEcBlindingFactorLength);
Buffer negated(kEcBlindingFactorLength);
secp256k1_mpt_scalar_add(sum.data(), lhs.data(), rhs.data());
secp256k1_mpt_scalar_negate(negated.data(), sum.data());
return negated;
};

Buffer const canonicalZeroBlindingFactor =
getCanonicalZeroBlindingFactor(bob.id(), mptAlice.issuanceID());
Buffer const maliciousSendBlindingFactor =
negateScalarSum(canonicalZeroBlindingFactor, convertBlindingFactor);

// This leaves Bob's inbox with randomness -r0, making
// MergeInbox hit the point at infinity.
mptAlice.send({
.account = carol,
.dest = bob,
.amt = 5,
.blindingFactor = maliciousSendBlindingFactor,
});

mptAlice.mergeInbox({.account = bob});

auto const bobSpending =
mptAlice.getDecryptedBalance(bob, MPTTester::HolderEncryptedSpending);
BEAST_EXPECT(bobSpending && *bobSpending == 25);
}

void
testWithFeats(FeatureBitset features)
{
Expand Down Expand Up @@ -7931,6 +8047,7 @@ class ConfidentialTransferTest : public ConfidentialTransferTestBase
testSendCiphertextCombination(features);
testSendCiphertextRerandomization(features);
testSendZeroRandomnessCiphertext(features);
testSendRerandomizesRecipientInboxAgainstMergeCancellation(features);
}

public:
Expand All @@ -7945,10 +8062,6 @@ class ConfidentialTransferTest : public ConfidentialTransferTestBase
};
// NOLINTEND(misc-const-correctness, bugprone-unchecked-optional-access)

// TEMPORARILY DISABLED: the ConfidentialTransfer suite is unusably slow because
// the test harness verifies balances via ElGamal decryption, which brute-forces
// a discrete log over a fixed iteration range in mpt-crypto. Re-enable (uncomment)
// once mpt-crypto ships the faster baby-step-giant-step decryption.
// BEAST_DEFINE_TESTSUITE(ConfidentialTransfer, app, xrpl);
BEAST_DEFINE_TESTSUITE(ConfidentialTransfer, app, xrpl);

} // namespace xrpl
Loading