add homomorphic tests (#6490)

This commit is contained in:
Peter Chen
2026-03-16 08:27:36 -07:00
committed by GitHub
parent d615098849
commit c45177b69e

View File

@@ -5620,6 +5620,277 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.err = tecBAD_PROOF}); // Fails because TransactionContextID differs
}
/* This test simulates an attack where the holder ciphertext is modified
* via homomorphic addition (adding Encrypted_amt(1)) while leaving the issuer
* ciphertext unchanged. It confirms that the validator detects the
* mismatch between the re-computed ciphertexts and the submitted ones,
* resulting in tecBAD_PROOF. */
void
testHomomorphicCiphertextModification(FeatureBitset features)
{
testcase("Homomorphic ciphertext modification");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy});
mptAlice.authorize({.account = bob});
mptAlice.pay(alice, bob, 100);
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
mptAlice.generateKeyPair(bob);
// Bob converts 50 to confidential balance
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
mptAlice.mergeInbox({.account = bob});
// Prepare valid parameters for a ConvertBack of 10
uint64_t const amt = 10;
Buffer const bf = generateBlindingFactor();
auto const holderCipherText = mptAlice.encryptAmount(bob, amt, bf);
auto const issuerCipherText = mptAlice.encryptAmount(alice, amt, bf);
// Generate a "Delta" ciphertext (Encrypting 1)
// We use Bob's key because we are tampering with Bob's (Holder's) field
Buffer const deltaBf = generateBlindingFactor();
auto const deltaCipherText = mptAlice.encryptAmount(bob, 1, deltaBf);
// Homomorphically add Delta to HolderCipherText: Tampered = Enc(10) + Enc(1) = Enc(11)
auto tamperedOpt = homomorphicAdd(holderCipherText, deltaCipherText);
BEAST_EXPECT(tamperedOpt.has_value());
Buffer tamperedHolderCipherText = std::move(*tamperedOpt);
// Generate a valid proof for the ORIGINAL amount (10)
auto const spendingBal =
mptAlice.getDecryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
auto const spendingBalEnc =
mptAlice.getEncryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
Buffer const pcBf = generateBlindingFactor();
auto const pedersenCommitment = mptAlice.getPedersenCommitment(*spendingBal, pcBf);
auto const currentVersion = mptAlice.getMPTokenVersion(bob);
// Uses the new signature: Account, IssuanceID, Sequence, Version
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion);
Buffer const proof = mptAlice.getConvertBackProof(
bob,
amt,
contextHash,
{
.pedersenCommitment = pedersenCommitment,
.amt = *spendingBal,
.encryptedAmt = *spendingBalEnc,
.blindingFactor = pcBf,
});
// Submit transaction with Divergent Ciphertexts
// Holder Ciphertext encrypts 11. Issuer Ciphertext encrypts 10.
// The consistency check (re-encryption of `amt` with `bf`) will match Issuer but FAIL for
// Holder.
mptAlice.convertBack(
{.account = bob,
.amt = amt,
.proof = proof,
.holderEncryptedAmt = tamperedHolderCipherText, // Tampered (11)
.issuerEncryptedAmt = issuerCipherText, // Original (10)
.blindingFactor = bf,
.pedersenCommitment = pedersenCommitment,
.err = tecBAD_PROOF});
}
/* This test verifies that rippled correctly rejects attempts to
* overflow the maximum allowable token amount via homomorphic manipulation.
* It simulates an attack where an individual takes a valid ciphertext encrypting
* the maximum amount (maxMPTokenAmount) and homomorphically adds an encryption of
* 1 to it, producing a ciphertext for MAX+1. The test confirms that the Bulletproof
* range proof or inner-product constraints detect this overflow and invalidate the
* transaction, preserving the supply invariant. */
void
testSendHomomorphicOverflow(FeatureBitset features)
{
testcase("Send: homomorphic overflow attack via Enc(MAX) + Enc(1)");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanPrivacy | tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 100);
mptAlice.pay(alice, carol, 50);
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(carol);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
mptAlice.convert({.account = bob, .amt = 100, .holderPubKey = mptAlice.getPubKey(bob)});
mptAlice.mergeInbox({.account = bob});
mptAlice.convert({.account = carol, .amt = 50, .holderPubKey = mptAlice.getPubKey(carol)});
mptAlice.mergeInbox({.account = carol});
// Bob sends 10 to carol. The send amount (10) and Bob's remaining balance
// (90) are both within [0, maxMPTokenAmount]. Range proof passes.
mptAlice.send({.account = bob, .dest = carol, .amt = 10});
// Bob's spending balance is 90 after the baseline send.
auto const bobSpendingBefore =
mptAlice.getDecryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
BEAST_EXPECT(bobSpendingBefore == 90);
// Construct Enc(maxMPTokenAmount) with Bob's public key.
Buffer const bf1 = generateBlindingFactor();
Buffer const encMax = mptAlice.encryptAmount(bob, maxMPTokenAmount, bf1);
// Construct Enc(1) with a separate blinding factor.
Buffer const bf2 = generateBlindingFactor();
Buffer const encOne = mptAlice.encryptAmount(bob, 1, bf2);
// Homomorphically add to produce CB_S_holder' = Enc(MAX) + Enc(1)
auto overflowedOpt = homomorphicAdd(encMax, encOne);
BEAST_EXPECT(overflowedOpt.has_value());
Buffer overflowedCt = std::move(*overflowedOpt);
// Submit the send transaction with the tampered ciphertext.
// Setting amt = maxMPTokenAmount + 1 drives proof generation for the
// overflowed value. The bulletproof range check [0, maxMPTokenAmount]
// rejects MAX+1; the validator must return tecBAD_PROOF.
mptAlice.send({
.account = bob,
.dest = carol,
.amt = maxMPTokenAmount + 1,
.senderEncryptedAmt = overflowedCt,
.err = tecBAD_PROOF,
});
auto const bobSpendingAfter =
mptAlice.getDecryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
BEAST_EXPECT(bobSpendingBefore == bobSpendingAfter);
}
/* This test ensures that the system prevents underflow attacks where a user
* attempts to create a negative balance through homomorphic subtraction. It
* simulates a scenario where an attacker takes a ciphertext encrypting zero
* and subtracts an encryption of 1, resulting in a value of -1.
* The test asserts that the range proof verification fails because the resulting
* value falls outside the valid non-negative range [0, maxMPTokenAmount],
* causing the validator to reject the transaction with tecBAD_PROOF. */
void
testConvertBackHomomorphicUnderflow(FeatureBitset features)
{
testcase("ConvertBack: homomorphic underflow attack via Enc(0) - Enc(1)");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy});
mptAlice.authorize({.account = bob});
mptAlice.pay(alice, bob, 10);
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
mptAlice.convert({.account = bob, .amt = 10, .holderPubKey = mptAlice.getPubKey(bob)});
mptAlice.mergeInbox({.account = bob});
// Converting back 1 from 10 leaves remaining balance = 9 (non-negative).
// Range proof [0, maxMPTokenAmount] passes.
mptAlice.convertBack({.account = bob, .amt = 1});
// Bob's spending balance is now 9; public balance is 1.
auto const bobSpendingBefore =
mptAlice.getDecryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
BEAST_EXPECT(bobSpendingBefore == 9);
auto const bobPublicBefore = mptAlice.getBalance(bob);
BEAST_EXPECT(bobPublicBefore == 1);
// Construct Enc(0) — the zero encrypted balance using Bob's key.
Buffer const bf1 = generateBlindingFactor();
Buffer const encZero = mptAlice.encryptAmount(bob, 0, bf1);
// Construct Enc(1) with a separate blinding factor.
Buffer const bf2 = generateBlindingFactor();
Buffer const encOne = mptAlice.encryptAmount(bob, 1, bf2);
// Homomorphically subtract to produce CB_S_holder' = Enc(0) Enc(1)
// = Enc(1), which lies below [0, maxMPTokenAmount].
auto underflowedOpt = homomorphicSubtract(encZero, encOne);
BEAST_EXPECT(underflowedOpt.has_value());
Buffer underflowedCt = std::move(*underflowedOpt);
// The underflowed value as uint64_t: 0 - 1 wraps to 0xFFFFFFFFFFFFFFFF.
// Generate a real proof using this wrapped value. The validator must still reject it
// because 0xFFFFFFFFFFFFFFFE (remaining balance) is outside [0, maxMPTokenAmount].
constexpr std::uint64_t underflowedAmt =
static_cast<std::uint64_t>(0) - static_cast<std::uint64_t>(1);
Buffer const pcBf = generateBlindingFactor();
Buffer const pedersenCommitment = mptAlice.getPedersenCommitment(underflowedAmt, pcBf);
auto const currentVersion = mptAlice.getMPTokenVersion(bob);
uint256 const contextHash =
getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion);
Buffer const proof = mptAlice.getConvertBackProof(
bob,
1,
contextHash,
{
.pedersenCommitment = pedersenCommitment,
.amt = underflowedAmt,
.encryptedAmt = underflowedCt,
.blindingFactor = pcBf,
});
mptAlice.convertBack({
.account = bob,
.amt = 1,
.proof = proof,
.holderEncryptedAmt = underflowedCt,
.pedersenCommitment = pedersenCommitment,
.err = tecBAD_PROOF,
});
// Supply invariant: both public and confidential balances must be unchanged
// after the rejected attack.
BEAST_EXPECT(mptAlice.getBalance(bob) == bobPublicBefore);
auto const bobSpendingAfter =
mptAlice.getDecryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
BEAST_EXPECT(bobSpendingBefore == bobSpendingAfter);
}
void
testWithFeats(FeatureBitset features)
{
@@ -5662,6 +5933,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
testConvertBackPedersenProof(features);
testConvertBackBulletproof(features);
// Homomorphic operation tests
testSendHomomorphicOverflow(features);
testHomomorphicCiphertextModification(features);
testConvertBackHomomorphicUnderflow(features);
// Replay Tests
testMutatePrivacy(features);
testProofContextBinding(features);
testProofCiphertextBinding(features);