diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index e00f1e57b8..2005c16550 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -320,6 +320,9 @@ std::size_t constexpr ecSchnorrProofLength = 65; /** Length of ElGamal Pedersen linkage proof */ std::size_t constexpr ecPedersenProofLength = 195; + +/** Length of Pedersen Commitment proof */ +std::size_t constexpr ecPedersenCommitmentLength = 64; } // namespace ripple #endif diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index ef1cc24048..7fd8fb918a 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -313,6 +313,7 @@ TYPED_SFIELD(sfAuditorEncryptedBalance, VL, 42) TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43) TYPED_SFIELD(sfAuditorElGamalPublicKey, VL, 44) TYPED_SFIELD(sfBlindingFactor, VL, 45) +TYPED_SFIELD(sfPedersenCommitment, VL, 46) // account (common) TYPED_SFIELD(sfAccount, ACCOUNT, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index a9154a2e6c..3d73c9599c 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1107,6 +1107,7 @@ TRANSACTION(ttCONFIDENTIAL_CONVERT_BACK, 87, ConfidentialConvertBack, {sfAuditorEncryptedAmount, soeOPTIONAL}, {sfBlindingFactor, soeREQUIRED}, {sfZKProof, soeREQUIRED}, + {sfPedersenCommitment, soeREQUIRED} })) #if TRANSACTION_INCLUDE diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index 6ebf7ec4ba..851ef624ee 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -428,9 +428,9 @@ verifyPedersenLinkage( if (secp256k1_elgamal_pedersen_link_verify( secp256k1Context(), proof.data(), - &c1, - &c2, &pubKey, + &c2, + &c1, &pcm, contextHash.data()) != 1) return tecBAD_PROOF; diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index ab6019e578..e526f8e09f 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -3026,6 +3026,201 @@ class ConfidentialTransfer_test : public beast::unit_test::suite } } + void + testConvertBackProof(FeatureBitset features) + { + testcase("Convert back proof"); + 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); + + mptAlice.convert({ + .account = bob, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.mergeInbox({ + .account = bob, + }); + + // for ease of understanding, generate all the fields here instead of + // autofilling + uint64_t const amt = 10; + Buffer const blindingFactor = generateBlindingFactor(); + Buffer const pcBlindingFactor = generateBlindingFactor(); + uint64_t const spendingBalance = mptAlice.getDecryptedBalance( + bob, MPTTester::HOLDER_ENCRYPTED_SPENDING); + auto const encryptedSpendingBalance = mptAlice.getEncryptedBalance( + bob, MPTTester::HOLDER_ENCRYPTED_SPENDING); + + BEAST_EXPECT(encryptedSpendingBalance); + + Buffer const pedersenCommitment = + mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor); + Buffer const issuerCiphertext = + mptAlice.encryptAmount(alice, amt, blindingFactor); + Buffer const bobCiphertext = + mptAlice.encryptAmount(bob, amt, blindingFactor); + auto const version = mptAlice.getMPTokenVersion(bob); + + // generate a proof using a pedersen commitment using the wrong value + { + uint256 const contextHash = getConvertBackContextHash( + bob, env.seq(bob), mptAlice.issuanceID(), amt, version); + Buffer const badPedersenCommitment = + mptAlice.getPedersenCommitment(1, pcBlindingFactor); + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + bobCiphertext, + issuerCiphertext, + {}, + blindingFactor, + { + .pedersenCommitment = + badPedersenCommitment, // bad pedersen commitment + .amt = spendingBalance, + .encryptedAmt = *encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack( + {.account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF}); + } + + // test when the pedersen commitment is wrong while the proof is + // right + { + // generate the context hash again because bob's sequence + // incremented from prev txn + uint256 const contextHash = getConvertBackContextHash( + bob, env.seq(bob), mptAlice.issuanceID(), amt, version); + + Buffer const badPedersenCommitment = + mptAlice.getPedersenCommitment(1, pcBlindingFactor); + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + bobCiphertext, + issuerCiphertext, + {}, + blindingFactor, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = *encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack( + {.account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = + badPedersenCommitment, // wrong pc used here + .err = tecBAD_PROOF}); + } + + // the pc blinding factor for generating the pc is different from the + // one used to generate pedersen proof + { + // generate the context hash again because bob's sequence + // incremented from prev txn + uint256 const contextHash = getConvertBackContextHash( + bob, env.seq(bob), mptAlice.issuanceID(), amt, version); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + bobCiphertext, + issuerCiphertext, + {}, + blindingFactor, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = *encryptedSpendingBalance, + .blindingFactor = + generateBlindingFactor(), // bad blinding factor + }); + + mptAlice.convertBack( + {.account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF}); + } + + // a correct proof + { + // generate the context hash again because bob's sequence + // incremented from prev txn + uint256 const contextHash = getConvertBackContextHash( + bob, env.seq(bob), mptAlice.issuanceID(), amt, version); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + bobCiphertext, + issuerCiphertext, + {}, + blindingFactor, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = *encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + }); + } + } + void testWithFeats(FeatureBitset features) { @@ -3060,6 +3255,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite testConvertBackPreflight(features); testConvertBackPreclaim(features); testConvertBackWithAuditor(features); + testConvertBackProof(features); testMutatePrivacy(features); } diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index fa04ec55b8..7459b1c7c6 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -140,7 +140,8 @@ MPTTester::MPTTester(MPTInitDef const& arg) { } -MPTTester::operator MPT() const +MPTTester:: +operator MPT() const { if (!id_) Throw("MPT has not been created"); @@ -694,7 +695,8 @@ MPTTester::mpt(std::int64_t amount) const return ripple::test::jtx::MPT(issuer_.name(), *id_)(amount); } -MPTTester::operator Asset() const +MPTTester:: +operator Asset() const { if (!id_) Throw("MPT has not been created"); @@ -736,7 +738,7 @@ MPTTester::getClawbackProof( Account const& holder, std::uint64_t amount, Buffer const& privateKey, - uint256 const& ctxHash) const + uint256 const& contextHash) const { if (!id_) Throw("MPT has not been created"); @@ -794,7 +796,7 @@ MPTTester::getClawbackProof( &c1, amount, privateKey.data(), - ctxHash.data()) != 1) + contextHash.data()) != 1) { Throw("Proof generation failed"); } @@ -803,7 +805,8 @@ MPTTester::getClawbackProof( } Buffer -MPTTester::getSchnorrProof(Account const& account, uint256 const& ctxHash) const +MPTTester::getSchnorrProof(Account const& account, uint256 const& contextHash) + const { auto const pubKey = getPubKey(account); auto const privKey = getPrivKey(account); @@ -821,7 +824,7 @@ MPTTester::getSchnorrProof(Account const& account, uint256 const& ctxHash) const proof.data(), &pk, privKey.data(), - ctxHash.data()) != 1) + contextHash.data()) != 1) { Throw("Schnorr Proof generation failed"); } @@ -829,19 +832,53 @@ MPTTester::getSchnorrProof(Account const& account, uint256 const& ctxHash) const return proof; } +Buffer +MPTTester::getPedersenCommitment( + std::uint64_t const amount, + Buffer const& pedersenBlindingFactor) +{ + // Blinding factor (rho) must be a 32-byte scalar + if (pedersenBlindingFactor.size() != ecBlindingFactorLength) + Throw("Invalid blinding factor size"); + + // current pedersen generation implementation fails if amount is 0 + if (amount == 0) + return Buffer{ecPedersenCommitmentLength}; + + secp256k1_pubkey commitment; + auto const ctx = secp256k1Context(); + + // Compute PC = m*G + rho*H + if (secp256k1_mpt_pedersen_commit( + ctx, &commitment, amount, pedersenBlindingFactor.data()) != 1) + { + Throw("Pedersen commitment generation failed"); + } + + return Buffer{commitment.data, ecPedersenCommitmentLength}; +} + Buffer MPTTester::getConvertBackProof( Account const& holder, - std::uint64_t amount, - uint256 const& ctxHash, + std::uint64_t const amount, + uint256 const& contextHash, Buffer const& holderCiphertext, Buffer const& issuerCiphertext, std::optional const& auditorCiphertext, - Buffer const& blindingFactor) const + Buffer const& blindingFactor, + PedersenProofParams const& pcParams) const { - // todo: incoporate pederson and range proof + auto const sleMptoken = env_.le(keylet::mptoken(*id_, holder.id())); + if (!sleMptoken || + !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending)) + return Buffer{}; - return Buffer{}; + Buffer const pedersenProof = generatePedersenLinkageProof( + holder, contextHash, getPubKey(holder), pcParams); + + // todo: incoporate range proof + return pedersenProof; } std::optional @@ -992,10 +1029,10 @@ MPTTester::convert(MPTConvert const& arg) // if fillSchnorrProof is explicitly set, follow its value; // otherwise, default to generating the proof only if holder pub key is // present. - auto const ctxHash = getConvertContextHash( + auto const contextHash = getConvertContextHash( arg.account->id(), env_.seq(*arg.account), *id_, *arg.amt); - Buffer proof = getSchnorrProof(*arg.account, ctxHash); + Buffer proof = getSchnorrProof(*arg.account, contextHash); jv[sfZKProof.jsonName] = strHex(proof); } @@ -1276,10 +1313,10 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg) else { std::uint32_t const seq = env_.seq(account); - uint256 const ctxHash = getClawbackContextHash( + uint256 const contextHash = getClawbackContextHash( account.id(), seq, *id_, *arg.amt, arg.holder->id()); Buffer proof = getClawbackProof( - *arg.holder, *arg.amt, getPrivKey(account), ctxHash); + *arg.holder, *arg.amt, getPrivKey(account), contextHash); jv[sfZKProof] = strHex(proof); } @@ -1538,24 +1575,54 @@ MPTTester::convertBack(MPTConvertBack const& arg) jv[sfBlindingFactor] = strHex(blindingFactor); + uint64_t prevSpendingBalance = + getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + + Buffer pedersenCommitment; + Buffer pcBlindingFactor = generateBlindingFactor(); + if (arg.pedersenCommitment) + pedersenCommitment = *arg.pedersenCommitment; + else + pedersenCommitment = + getPedersenCommitment(prevSpendingBalance, pcBlindingFactor); + + jv[sfPedersenCommitment] = strHex(pedersenCommitment); + if (arg.proof) - jv[sfZKProof.jsonName] = *arg.proof; + jv[sfZKProof.jsonName] = strHex(*arg.proof); else { auto const version = getMPTokenVersion(*arg.account); // if the caller generated ciphertexts themselves, they should also // generate the proof themselves from the blinding factor - uint256 const ctxHash = getConvertBackContextHash( + uint256 const contextHash = getConvertBackContextHash( arg.account->id(), env_.seq(*arg.account), *id_, *arg.amt, version); - Buffer proof = getConvertBackProof( - *arg.account, - *arg.amt, - ctxHash, - holderCiphertext, - issuerCiphertext, - auditorCiphertext, - blindingFactor); + auto const prevEncryptedSpendingBalance = + getEncryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + + Buffer proof; + // generate a dummy proof if no encrypted amount field, so that other + // preflight/preclaim are checked + if (!prevEncryptedSpendingBalance) + proof = Buffer(); + else + { + proof = getConvertBackProof( + *arg.account, + *arg.amt, + contextHash, + holderCiphertext, + issuerCiphertext, + auditorCiphertext, + blindingFactor, + { + .pedersenCommitment = pedersenCommitment, + .amt = prevSpendingBalance, + .encryptedAmt = *prevEncryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + } jv[sfZKProof] = strHex(proof); } @@ -1564,8 +1631,6 @@ MPTTester::convertBack(MPTConvertBack const& arg) uint64_t prevInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); - uint64_t prevSpendingBalance = - getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); uint64_t prevIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); [[maybe_unused]] uint64_t prevAuditorBalance = @@ -1620,6 +1685,57 @@ MPTTester::convertBack(MPTConvertBack const& arg) } } +Buffer +MPTTester::generatePedersenLinkageProof( + Account const& account, + uint256 const& contextHash, + Buffer const& pubKey, + PedersenProofParams const& params) const +{ + if (params.blindingFactor.size() != ecBlindingFactorLength || + params.pedersenCommitment.size() != ecPedersenCommitmentLength || + pubKey.size() != ecPubKeyLength || + params.encryptedAmt.size() != ecGamalEncryptedTotalLength) + return Buffer(ecPedersenProofLength); + + secp256k1_pubkey c1, c2; + auto const ctx = secp256k1Context(); + if (!secp256k1_ec_pubkey_parse( + ctx, &c1, params.encryptedAmt.data(), ecGamalEncryptedLength) || + !secp256k1_ec_pubkey_parse( + ctx, + &c2, + params.encryptedAmt.data() + ecGamalEncryptedLength, + ecGamalEncryptedLength)) + { + return Buffer(); + } + + secp256k1_pubkey pk; + std::memcpy(pk.data, pubKey.data(), ecPubKeyLength); + + secp256k1_pubkey pcm; + std::memcpy( + pcm.data, params.pedersenCommitment.data(), ecPedersenCommitmentLength); + + Buffer proof(ecPedersenProofLength); + + if (secp256k1_elgamal_pedersen_link_prove( + ctx, + proof.data(), + &pk, + &c2, + &c1, + &pcm, + params.amt, + getPrivKey(account).data(), + params.blindingFactor.data(), + contextHash.data()) != 1) + Throw("Pedersen proof generation failed"); + + return proof; +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 6ef4fed80a..a3cbc466b1 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -187,7 +187,6 @@ struct MPTConvert std::optional issuerEncryptedAmt = std::nullopt; std::optional auditorEncryptedAmt = std::nullopt; - // not an txn param, only used for autofilling std::optional blindingFactor = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -231,12 +230,12 @@ struct MPTConvertBack std::optional account = std::nullopt; std::optional id = std::nullopt; std::optional amt = std::nullopt; - std::optional proof = std::nullopt; + std::optional proof = std::nullopt; std::optional holderEncryptedAmt = std::nullopt; std::optional issuerEncryptedAmt = std::nullopt; std::optional auditorEncryptedAmt = std::nullopt; - // not an txn param, only used for autofilling std::optional blindingFactor = std::nullopt; + std::optional pedersenCommitment = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; @@ -256,6 +255,18 @@ struct MPTConfidentialClawback std::optional err = std::nullopt; }; +/** + * @brief Stores the parameterss that are exclusively used to generate a + * pedersen linkage proof + */ +struct PedersenProofParams +{ + Buffer const pedersenCommitment; + uint64_t const amt; // either spending balance or value to be transferred + Buffer const encryptedAmt; + Buffer const blindingFactor; +}; + class MPTTester { Env& env_; @@ -454,21 +465,34 @@ public: uint256 const& txHash) const; Buffer - getSchnorrProof(Account const& account, uint256 const& ctxHash) const; + getSchnorrProof(Account const& account, uint256 const& contextHash) const; Buffer getConvertBackProof( Account const& holder, - std::uint64_t amount, - uint256 const& ctxHash, + std::uint64_t const amount, + uint256 const& contextHash, Buffer const& holderCiphertext, Buffer const& issuerCiphertext, std::optional const& auditorCiphertext, - Buffer const& blindingFactor) const; + Buffer const& blindingFactor, + PedersenProofParams const& pcParams) const; std::uint32_t getMPTokenVersion(Account const account) const; + Buffer + generatePedersenLinkageProof( + Account const& account, + uint256 const& contextHash, + Buffer const& pubKey, + PedersenProofParams const& params) const; + + Buffer + getPedersenCommitment( + std::uint64_t const amount, + Buffer const& pedersenBlindingFactor); + private: using SLEP = SLE::const_pointer; bool diff --git a/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp b/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp index b2d69ebd5e..493b7aa225 100644 --- a/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp @@ -28,6 +28,9 @@ ConfidentialConvertBack::preflight(PreflightContext const& ctx) if (ctx.tx[sfBlindingFactor].size() != ecBlindingFactorLength) return temMALFORMED; + if (ctx.tx[sfPedersenCommitment].size() != ecPedersenCommitmentLength) + return temMALFORMED; + // check encrypted amount format after the above basic checks // this check is more expensive so put it at the end if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res)) @@ -45,19 +48,18 @@ verifyProofs( if (!mptoken->isFieldPresent(sfHolderElGamalPublicKey)) return tecINTERNAL; // LCOV_EXCL_LINE - // auto const mptIssuanceID = tx[sfMPTokenIssuanceID]; - // auto const account = tx[sfAccount]; + auto const mptIssuanceID = tx[sfMPTokenIssuanceID]; + auto const account = tx[sfAccount]; auto const amount = tx[sfMPTAmount]; auto const blindingFactor = tx[sfBlindingFactor]; auto const holderPubKey = (*mptoken)[sfHolderElGamalPublicKey]; - // todo: commented out for now, will use for range proof - // auto const contextHash = getConvertBackContextHash( - // account, - // tx[sfSequence], - // mptIssuanceID, - // amount, - // (*mptoken)[~sfConfidentialBalanceVersion].value_or(0)); + auto const contextHash = getConvertBackContextHash( + account, + tx[sfSequence], + mptIssuanceID, + amount, + (*mptoken)[~sfConfidentialBalanceVersion].value_or(0)); // Prepare Auditor Info std::optional auditor; @@ -82,6 +84,28 @@ verifyProofs( return ter; } + // Use a pointer to parse each proof component + Buffer zkps = Buffer(tx[sfZKProof].data(), tx[sfZKProof].size()); + std::uint8_t* ptr = zkps.data(); + + // verify el gamal pedersen linkage + { + Buffer const pedersen{ptr, ecPedersenProofLength}; + if (auto const ter = verifyPedersenLinkage( + pedersen, + (*mptoken)[sfConfidentialBalanceSpending], + holderPubKey, + tx[sfPedersenCommitment], + contextHash); + !isTesSuccess(ter)) + { + return ter; + } + + // increment pointer + ptr += ecPedersenProofLength; + } + return tesSUCCESS; }