Re-randomize after each send (#7535)

This commit is contained in:
yinyiqian1
2026-06-16 21:11:42 -04:00
committed by GitHub
parent 729bc70ec8
commit 31a3a0db62
7 changed files with 172 additions and 12 deletions

View File

@@ -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",
@@ -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"
],

View File

@@ -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",
@@ -134,6 +133,7 @@ class Xrpl(ConanFile):
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:

View File

@@ -243,8 +243,10 @@ words:
- Raphson
- rcflags
- replayer
- rerandomize
- rerandomization
- rerandomized
- rerandomizes
- rerere
- retriable
- RIPD

View File

@@ -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.
*

View File

@@ -220,6 +220,16 @@ homomorphicSubtract(Slice const& a, Slice const& b)
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;
return homomorphicAdd(ciphertext, *zero);
}
Buffer
generateBlindingFactor()
{

View File

@@ -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>
@@ -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.
@@ -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];
@@ -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
@@ -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

View File

@@ -11,6 +11,7 @@
#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>
@@ -18,6 +19,7 @@
#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>
@@ -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>
@@ -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)
@@ -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)
{
@@ -7931,6 +8047,7 @@ class ConfidentialTransferTest : public ConfidentialTransferTestBase
testSendCiphertextCombination(features);
testSendCiphertextRerandomization(features);
testSendZeroRandomnessCiphertext(features);
testSendRerandomizesRecipientInboxAgainstMergeCancellation(features);
}
public:
@@ -7945,10 +8062,6 @@ public:
};
// 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