mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 23:30:54 +00:00
Re-randomize after each send (#7535)
This commit is contained in:
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -243,8 +243,10 @@ words:
|
||||
- Raphson
|
||||
- rcflags
|
||||
- replayer
|
||||
- rerandomize
|
||||
- rerandomization
|
||||
- rerandomized
|
||||
- rerandomizes
|
||||
- rerere
|
||||
- retriable
|
||||
- RIPD
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user