diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index 94f8125f9e..aded57ab5d 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -163,8 +163,49 @@ verifyClawbackEqualityProof( Buffer generateBlindingFactor(); +/** + * @brief Verifies the cryptographic link between an ElGamal Ciphertext and a + * Pedersen Commitment for a transaction Amount. + * + * It proves that the ElGamal ciphertext `encAmt` encrypts the same value `m` + * as the Pedersen Commitment `pcmSlice`, using the randomness `r`. + * Proves Enc(m) <-> Pcm(m) + * + * @param proof The Zero Knowledge Proof bytes. + * @param encAmt The ElGamal ciphertext of the amount (C1, C2). + * @param pubKeySlice The sender's public key. + * @param pcmSlice The Pedersen Commitment to the amount. + * @param contextHash The unique context hash for this transaction. + * @return tesSUCCESS if the proof is valid, or an error code otherwise. + */ TER -verifyPedersenLinkage( +verifyAmountPcmLinkage( + Slice const& proof, + Slice const& encAmt, + Slice const& pubKeySlice, + Slice const& pcmSlice, + uint256 const& contextHash); + +/** + * @brief Verifies the cryptographic link between an ElGamal Ciphertext and a + * Pedersen Commitment for an account Balance. + * + * It proves that the ElGamal ciphertext `encAmt` encrypts the same value `b` + * as the Pedersen Commitment `pcmSlice`, using the secret key `s`. + * Proves Enc(b) <-> Pcm(b) + * + * Note: Swaps arguments (Pk <-> C1) to accommodate the different algebraic + * structure. + * + * @param proof The Zero Knowledge Proof bytes. + * @param encAmt The ElGamal ciphertext of the balance (C1, C2). + * @param pubKeySlice The sender's public key. + * @param pcmSlice The Pedersen Commitment to the balance. + * @param contextHash The unique context hash for this transaction. + * @return tesSUCCESS if the proof is valid, or an error code otherwise. + */ +TER +verifyBalancePcmLinkage( Slice const& proof, Slice const& encAmt, Slice const& pubKeySlice, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 7fd8fb918a..04e0beac56 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -313,7 +313,8 @@ 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) +TYPED_SFIELD(sfAmountCommitment, VL, 46) +TYPED_SFIELD(sfBalanceCommitment, VL, 47) // account (common) TYPED_SFIELD(sfAccount, ACCOUNT, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 3d73c9599c..f681a6e2f1 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1107,7 +1107,7 @@ TRANSACTION(ttCONFIDENTIAL_CONVERT_BACK, 87, ConfidentialConvertBack, {sfAuditorEncryptedAmount, soeOPTIONAL}, {sfBlindingFactor, soeREQUIRED}, {sfZKProof, soeREQUIRED}, - {sfPedersenCommitment, soeREQUIRED} + {sfBalanceCommitment, soeREQUIRED}, })) #if TRANSACTION_INCLUDE @@ -1123,9 +1123,11 @@ TRANSACTION(ttCONFIDENTIAL_SEND, 88, ConfidentialSend, {sfSenderEncryptedAmount, soeREQUIRED}, {sfDestinationEncryptedAmount, soeREQUIRED}, {sfIssuerEncryptedAmount, soeREQUIRED}, - {sfZKProof, soeREQUIRED}, - {sfCredentialIDs, soeOPTIONAL}, {sfAuditorEncryptedAmount, soeOPTIONAL}, + {sfZKProof, soeREQUIRED}, + {sfAmountCommitment, soeREQUIRED}, + {sfBalanceCommitment, soeREQUIRED}, + {sfCredentialIDs, soeOPTIONAL}, })) #if TRANSACTION_INCLUDE diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index 1a3e1e109f..7bee2c20d7 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -479,7 +479,48 @@ checkEncryptedAmountFormat(STObject const& object) } TER -verifyPedersenLinkage( +verifyAmountPcmLinkage( + Slice const& proof, + Slice const& encAmt, + Slice const& pubKeySlice, + Slice const& pcmSlice, + uint256 const& contextHash) +{ + if (proof.length() != ecPedersenProofLength) + return tecINTERNAL; + + secp256k1_pubkey c1, c2; + if (!makeEcPair(encAmt, c1, c2)) + return tecINTERNAL; // LCOV_EXCL_LINE + + secp256k1_pubkey pubKey; + if (pubKeySlice.size() != ecPubKeyLength) + return tecINTERNAL; // LCOV_EXCL_LINE + + secp256k1_pubkey pcm; + if (pcmSlice.size() != ecPedersenCommitmentLength) + return tecINTERNAL; // LCOV_EXCL_LINE + + std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength); + std::memcpy(pcm.data, pcmSlice.data(), ecPedersenCommitmentLength); + + if (secp256k1_elgamal_pedersen_link_verify( + secp256k1Context(), + proof.data(), + &c1, + &c2, + &pubKey, + &pcm, + contextHash.data()) != 1) + { + return tecBAD_PROOF; + } + + return tesSUCCESS; +} + +TER +verifyBalancePcmLinkage( Slice const& proof, Slice const& encAmt, Slice const& pubKeySlice, @@ -493,12 +534,17 @@ verifyPedersenLinkage( secp256k1_pubkey c2; if (!makeEcPair(encAmt, c1, c2)) - return tecINTERNAL; + return tecINTERNAL; // LCOV_EXCL_LINE secp256k1_pubkey pubKey; - std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength); + if (pubKeySlice.size() != ecPubKeyLength) + return tecINTERNAL; // LCOV_EXCL_LINE secp256k1_pubkey pcm; + if (pcmSlice.size() != ecPedersenCommitmentLength) + return tecINTERNAL; // LCOV_EXCL_LINE + + std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength); std::memcpy(pcm.data, pcmSlice.data(), ecPubKeyLength); if (secp256k1_elgamal_pedersen_link_verify( @@ -509,7 +555,9 @@ verifyPedersenLinkage( &c1, &pcm, contextHash.data()) != 1) + { return tecBAD_PROOF; + } return tesSUCCESS; } diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index 22b971596f..d2de666399 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -48,6 +48,30 @@ class ConfidentialTransfer_test : public beast::unit_test::suite return trivialCiphertext; } + static std::string const& + getTrivialSendProofHex(size_t nRecipients) + { + static std::string const trivialProofHex = [nRecipients]() { + size_t const sizeEquality = + getMultiCiphertextEqualityProofSize(nRecipients); + size_t const totalSize = sizeEquality + (2 * ecPedersenProofLength); + + Buffer buf(totalSize); + std::memset(buf.data(), 0, totalSize); + + for (std::size_t i = 0; i < totalSize; i += ecGamalEncryptedLength) + { + buf.data()[i] = 0x02; + if (i + ecGamalEncryptedLength - 1 < totalSize) + buf.data()[i + ecGamalEncryptedLength - 1] = 0x01; + } + + return strHex(buf); + }(); + + return trivialProofHex; + } + void testConvert(FeatureBitset features) { @@ -1026,12 +1050,9 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = carol, .amt = 10, - .senderEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .destEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .issuerEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), + .senderEncryptedAmt = Buffer(ecGamalEncryptedTotalLength), + .destEncryptedAmt = Buffer(ecGamalEncryptedTotalLength), + .issuerEncryptedAmt = Buffer(ecGamalEncryptedTotalLength), .err = temDISABLED}); } @@ -1073,26 +1094,11 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = alice, .dest = carol, .amt = 10, - .senderEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .destEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .issuerEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), .err = temMALFORMED}); // can not send to self mptAlice.send( - {.account = bob, - .dest = bob, - .amt = 10, - .senderEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .destEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .issuerEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .err = temMALFORMED}); + {.account = bob, .dest = bob, .amt = 10, .err = temMALFORMED}); // sender encrypted amount wrong length mptAlice.send( @@ -1100,59 +1106,51 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .dest = carol, .amt = 10, .senderEncryptedAmt = Buffer(10), - .destEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .issuerEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), .err = temBAD_CIPHERTEXT}); // dest encrypted amount wrong length mptAlice.send( {.account = bob, .dest = carol, .amt = 10, - .senderEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), .destEncryptedAmt = Buffer(10), - .issuerEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), .err = temBAD_CIPHERTEXT}); // issuer encrypted amount wrong length mptAlice.send( {.account = bob, .dest = carol, .amt = 10, - .senderEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .destEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), .issuerEncryptedAmt = Buffer(10), .err = temBAD_CIPHERTEXT}); - // auto const ciphertextHex = generatePlaceholderCiphertext(); - // sender encrypted amount malformed mptAlice.send( {.account = bob, .dest = carol, .amt = 10, - .senderEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), + .proof = getTrivialSendProofHex(3), + .senderEncryptedAmt = Buffer(ecGamalEncryptedTotalLength), + .amountCommitment = Buffer(ecPedersenCommitmentLength), + .balanceCommitment = Buffer(ecPedersenCommitmentLength), .err = temBAD_CIPHERTEXT}); // dest encrypted amount malformed mptAlice.send( {.account = bob, .dest = carol, .amt = 10, - .destEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), + .proof = getTrivialSendProofHex(3), + .destEncryptedAmt = Buffer(ecGamalEncryptedTotalLength), + .amountCommitment = Buffer(ecPedersenCommitmentLength), + .balanceCommitment = Buffer(ecPedersenCommitmentLength), .err = temBAD_CIPHERTEXT}); // issuer encrypted amount malformed mptAlice.send( {.account = bob, .dest = carol, .amt = 10, - .issuerEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), + .proof = getTrivialSendProofHex(3), + .issuerEncryptedAmt = Buffer(ecGamalEncryptedTotalLength), + .amountCommitment = Buffer(ecPedersenCommitmentLength), + .balanceCommitment = Buffer(ecPedersenCommitmentLength), .err = temBAD_CIPHERTEXT}); // invalid proof length @@ -1160,13 +1158,29 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = carol, .amt = 10, - .senderEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .destEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), - .issuerEncryptedAmt = - Buffer(ripple::ecGamalEncryptedTotalLength), .proof = std::string(10, 'A'), + .amountCommitment = Buffer(ecPedersenCommitmentLength), + .balanceCommitment = Buffer(ecPedersenCommitmentLength), + .err = temMALFORMED}); + + // invalid amount Pedersen commitment length + mptAlice.send( + {.account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(3), + .amountCommitment = Buffer(100), + .balanceCommitment = Buffer(ecPedersenCommitmentLength), + .err = temMALFORMED}); + + // invalid balance Pedersen commitment length + mptAlice.send( + {.account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(3), + .amountCommitment = Buffer(ecPedersenCommitmentLength), + .balanceCommitment = Buffer(100), .err = temMALFORMED}); } } @@ -1253,10 +1267,10 @@ class ConfidentialTransfer_test : public beast::unit_test::suite jv[sfSenderEncryptedAmount] = strHex(getTrivialCiphertext()); jv[sfDestinationEncryptedAmount] = strHex(getTrivialCiphertext()); jv[sfIssuerEncryptedAmount] = strHex(getTrivialCiphertext()); - - auto const dummyProofSize = - secp256k1_mpt_prove_same_plaintext_multi_size(3); - jv[sfZKProof.jsonName] = strHex(Buffer(dummyProofSize)); + jv[sfAmountCommitment] = strHex(Buffer(ecPedersenCommitmentLength)); + jv[sfBalanceCommitment] = + strHex(Buffer(ecPedersenCommitmentLength)); + jv[sfZKProof] = getTrivialSendProofHex(3); env(jv, ter(tecOBJECT_NOT_FOUND)); } @@ -1268,8 +1282,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = unknown, .amt = 10, + .senderEncryptedAmt = getTrivialCiphertext(), .destEncryptedAmt = getTrivialCiphertext(), .issuerEncryptedAmt = getTrivialCiphertext(), + .proof = getTrivialSendProofHex(3), + .amountCommitment = Buffer(ecPedersenCommitmentLength), + .balanceCommitment = Buffer(ecPedersenCommitmentLength), .err = tecNO_TARGET}); } @@ -1279,11 +1297,23 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = dave, .amt = 10, + .senderEncryptedAmt = getTrivialCiphertext(), + .destEncryptedAmt = getTrivialCiphertext(), + .issuerEncryptedAmt = getTrivialCiphertext(), + .proof = getTrivialSendProofHex(3), + .amountCommitment = Buffer(ecPedersenCommitmentLength), + .balanceCommitment = Buffer(ecPedersenCommitmentLength), .err = tecNO_PERMISSION}); mptAlice.send( {.account = dave, .dest = carol, .amt = 10, + .senderEncryptedAmt = getTrivialCiphertext(), + .destEncryptedAmt = getTrivialCiphertext(), + .issuerEncryptedAmt = getTrivialCiphertext(), + .proof = getTrivialSendProofHex(3), + .amountCommitment = Buffer(ecPedersenCommitmentLength), + .balanceCommitment = Buffer(ecPedersenCommitmentLength), .err = tecNO_PERMISSION}); } @@ -1293,7 +1323,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = eve, .amt = 10, + .senderEncryptedAmt = getTrivialCiphertext(), .destEncryptedAmt = getTrivialCiphertext(), + .issuerEncryptedAmt = getTrivialCiphertext(), + .proof = getTrivialSendProofHex(3), + .amountCommitment = Buffer(ecPedersenCommitmentLength), + .balanceCommitment = Buffer(ecPedersenCommitmentLength), .err = tecOBJECT_NOT_FOUND}); } @@ -1468,14 +1503,11 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .account = carol, }); - auto const dummyProofSize = - secp256k1_mpt_prove_same_plaintext_multi_size(3); - mptAlice.send( {.account = bob, .dest = carol, .amt = 10, - .proof = strHex(Buffer(dummyProofSize)), + .proof = getTrivialSendProofHex(3), .err = tecBAD_PROOF}); } } diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 267d71e3f3..9dc9041461 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -819,11 +819,14 @@ MPTTester::getSchnorrProof(Account const& account, uint256 const& ctxHash) const std::optional MPTTester::getConfidentialSendProof( + Account const& sender, std::uint64_t const amount, std::vector const& recipients, Slice const& blindingFactor, std::size_t const nRecipients, - uint256 const& contextHash) const + uint256 const& contextHash, + PedersenProofParams const& amountParams, + PedersenProofParams const& balanceParams) const { if (recipients.size() != nRecipients) return std::nullopt; @@ -831,6 +834,10 @@ MPTTester::getConfidentialSendProof( if (blindingFactor.size() != ecBlindingFactorLength) return std::nullopt; + auto const senderPubKey = getPubKey(sender); + if (!senderPubKey) + return std::nullopt; + auto const ctx = secp256k1Context(); std::vector r(nRecipients); @@ -874,14 +881,15 @@ MPTTester::getConfidentialSendProof( blindingFactor.data() + ecBlindingFactorLength); } - size_t proofLen = + size_t sizeEquality = secp256k1_mpt_prove_same_plaintext_multi_size(nRecipients); - Buffer proof(proofLen); + Buffer equalityProof(sizeEquality); + // Get the multi-ciphertext equality proof if (secp256k1_mpt_prove_same_plaintext_multi( ctx, - proof.data(), - &proofLen, + equalityProof.data(), + &sizeEquality, amount, nRecipients, r.data(), @@ -893,6 +901,31 @@ MPTTester::getConfidentialSendProof( return std::nullopt; } + auto const amountLinkageProof = getAmountLinkageProof( + *senderPubKey, + Buffer(blindingFactor.data(), ecBlindingFactorLength), + contextHash, + amountParams); + + auto const balanceLinkageProof = getBalanceLinkageProof( + sender, contextHash, *senderPubKey, balanceParams); + + auto const sizeAmountLinkage = amountLinkageProof.size(); + auto const sizeBalanceLinkage = balanceLinkageProof.size(); + + size_t const proofSize = + sizeEquality + sizeAmountLinkage + sizeBalanceLinkage; + Buffer proof(proofSize); + + auto ptr = proof.data(); + std::memcpy(ptr, equalityProof.data(), sizeEquality); + ptr += sizeEquality; + + std::memcpy(ptr, amountLinkageProof.data(), sizeAmountLinkage); + ptr += sizeAmountLinkage; + + std::memcpy(ptr, balanceLinkageProof.data(), sizeBalanceLinkage); + return proof; } @@ -941,7 +974,7 @@ MPTTester::getConvertBackProof( auto const holderPubKey = getPubKey(holder); if (holderPubKey) { - Buffer const pedersenProof = generatePedersenLinkageProof( + Buffer const pedersenProof = getBalanceLinkageProof( holder, contextHash, *holderPubKey, pcParams); // todo: incoporate range proof @@ -1264,7 +1297,72 @@ MPTTester::send(MPTConfidentialSend const& arg) if (auditorAmt) jv[sfAuditorEncryptedAmount] = strHex(*auditorAmt); - // fill in the proof if not provided + if (arg.credentials) + { + auto& arr(jv[sfCredentialIDs.jsonName] = Json::arrayValue); + for (auto const& hash : *arg.credentials) + arr.append(hash); + } + + // Sender's previous confidential state + auto const prevSenderInbox = + getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); + auto const prevSenderSpending = + getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + auto const prevSenderIssuer = + getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); + if (!prevSenderInbox || !prevSenderSpending || !prevSenderIssuer) + Throw("Failed to get Pre-send balance"); + + std::optional prevSenderAuditor; + if (arg.auditorEncryptedAmt || auditor_) + { + prevSenderAuditor = + getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); + if (!prevSenderAuditor) + Throw("Failed to get Pre-send balance"); + } + + // Destination's previous confidential state + auto const prevDestInbox = + getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX); + auto const prevDestSpending = + getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING); + auto const prevDestIssuer = + getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE); + if (!prevDestInbox || !prevDestSpending || !prevDestIssuer) + Throw("Failed to get Pre-send balance"); + + std::optional prevDestAuditor; + if (arg.auditorEncryptedAmt || auditor_) + { + prevDestAuditor = + getDecryptedBalance(*arg.dest, AUDITOR_ENCRYPTED_BALANCE); + if (!prevDestAuditor) + Throw("Failed to get Pre-send balance"); + } + + // Fill in the commitment if not provided + Buffer amountCommitment, balanceCommitment; + auto const amountBlindingFactor = generateBlindingFactor(); + if (arg.amountCommitment) + amountCommitment = *arg.amountCommitment; + else + amountCommitment = + getPedersenCommitment(*arg.amt, amountBlindingFactor); + + jv[sfAmountCommitment] = strHex(amountCommitment); + + auto const balanceBlindingFactor = generateBlindingFactor(); + if (arg.balanceCommitment) + balanceCommitment = *arg.balanceCommitment; + else + balanceCommitment = + getPedersenCommitment(*prevSenderSpending, balanceBlindingFactor); + + jv[sfBalanceCommitment] = strHex(balanceCommitment); + + // Fill in the proof if not provided if (arg.proof) jv[sfZKProof] = *arg.proof; else @@ -1306,8 +1404,33 @@ MPTTester::send(MPTConfidentialSend const& arg) recipients.push_back({Slice(*auditorPubKey), *auditorAmt}); } - auto const proof = getConfidentialSendProof( - *arg.amt, recipients, blindingFactor, nRecipients, ctxHash); + auto const prevEncryptedSenderSpending = + getEncryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + + std::optional proof; + + // Skip proof generation if encrypted balance is missing (e.g., + // feature disabled), or when the sender and destination are the + // same (malformed case causing pcm to be zero). This prevents a + // crash and allows certain error cases to be tested. + if (arg.account != arg.dest && prevEncryptedSenderSpending) + { + proof = getConfidentialSendProof( + *arg.account, + *arg.amt, + recipients, + blindingFactor, + nRecipients, + ctxHash, + {.pedersenCommitment = amountCommitment, + .amt = *arg.amt, + .encryptedAmt = senderAmt, + .blindingFactor = amountBlindingFactor}, + {.pedersenCommitment = balanceCommitment, + .amt = *prevSenderSpending, + .encryptedAmt = *prevEncryptedSenderSpending, + .blindingFactor = balanceBlindingFactor}); + } if (proof) jv[sfZKProof.jsonName] = strHex(*proof); @@ -1320,55 +1443,11 @@ MPTTester::send(MPTConfidentialSend const& arg) } } - if (arg.credentials) - { - auto& arr(jv[sfCredentialIDs.jsonName] = Json::arrayValue); - for (auto const& hash : *arg.credentials) - arr.append(hash); - } - auto const senderPubAmt = getBalance(*arg.account); auto const destPubAmt = getBalance(*arg.dest); auto const prevCOA = getIssuanceConfidentialBalance(); auto const prevOA = getIssuanceOutstandingBalance(); - // Sender's previous confidential state - auto const prevSenderInbox = - getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); - auto const prevSenderSpending = - getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); - auto const prevSenderIssuer = - getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); - if (!prevSenderInbox || !prevSenderSpending || !prevSenderIssuer) - Throw("Failed to get Pre-send balance"); - - std::optional prevSenderAuditor; - if (arg.auditorEncryptedAmt || auditor_) - { - prevSenderAuditor = - getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); - if (!prevSenderAuditor) - Throw("Failed to get Pre-send balance"); - } - // Destination's previous confidential state - auto const prevDestInbox = - getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX); - auto const prevDestSpending = - getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING); - auto const prevDestIssuer = - getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE); - if (!prevDestInbox || !prevDestSpending || !prevDestIssuer) - Throw("Failed to get Pre-send balance"); - - std::optional prevDestAuditor; - if (arg.auditorEncryptedAmt || auditor_) - { - prevDestAuditor = - getDecryptedBalance(*arg.dest, AUDITOR_ENCRYPTED_BALANCE); - if (!prevDestAuditor) - Throw("Failed to get Pre-send balance"); - } - if (submit(arg, jv) == tesSUCCESS) { auto const postCOA = getIssuanceConfidentialBalance(); @@ -1797,7 +1876,7 @@ MPTTester::convertBack(MPTConvertBack const& arg) pedersenCommitment = getPedersenCommitment(*prevSpendingBalance, pcBlindingFactor); - jv[sfPedersenCommitment] = strHex(pedersenCommitment); + jv[sfBalanceCommitment] = strHex(pedersenCommitment); if (arg.proof) jv[sfZKProof.jsonName] = strHex(*arg.proof); @@ -1908,7 +1987,60 @@ MPTTester::convertBack(MPTConvertBack const& arg) } Buffer -MPTTester::generatePedersenLinkageProof( +MPTTester::getAmountLinkageProof( + Buffer const& pubKey, + Buffer const& blindingFactor, + uint256 const& contextHash, + PedersenProofParams const& params) const +{ + if (params.blindingFactor.size() != ecBlindingFactorLength || + params.pedersenCommitment.size() != ecPedersenCommitmentLength || + pubKey.size() != ecPubKeyLength || + params.encryptedAmt.size() != ecGamalEncryptedTotalLength || + blindingFactor.size() != ecBlindingFactorLength) + 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(), + &c1, + &c2, + &pk, + &pcm, + params.amt, + blindingFactor.data(), + params.blindingFactor.data(), + contextHash.data()) != 1) + { + Throw("Amount Linkage Proof generation failed"); + } + + return proof; +} + +Buffer +MPTTester::getBalanceLinkageProof( Account const& account, uint256 const& contextHash, Buffer const& pubKey, diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 0b2d34221d..da1bf9e2a7 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -215,6 +215,8 @@ struct MPTConfidentialSend std::optional> credentials = std::nullopt; // not an txn param, only used for autofilling std::optional blindingFactor = std::nullopt; + std::optional amountCommitment = std::nullopt; + std::optional balanceCommitment = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; @@ -465,11 +467,14 @@ public: std::optional getConfidentialSendProof( + Account const& sender, std::uint64_t const amount, std::vector const& recipients, Slice const& blindingFactor, std::size_t const nRecipients, - uint256 const& contextHash) const; + uint256 const& contextHash, + PedersenProofParams const& amountParams, + PedersenProofParams const& balanceParams) const; Buffer getConvertBackProof( @@ -486,7 +491,14 @@ public: getMPTokenVersion(Account const account) const; Buffer - generatePedersenLinkageProof( + getAmountLinkageProof( + Buffer const& pubKey, + Buffer const& blindingFactor, + uint256 const& contextHash, + PedersenProofParams const& params) const; + + Buffer + getBalanceLinkageProof( Account const& account, uint256 const& contextHash, Buffer const& pubKey, diff --git a/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp b/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp index 04e1cf4a8a..d40c814b04 100644 --- a/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp @@ -28,7 +28,7 @@ ConfidentialConvertBack::preflight(PreflightContext const& ctx) if (ctx.tx[sfBlindingFactor].size() != ecBlindingFactorLength) return temMALFORMED; - if (ctx.tx[sfPedersenCommitment].size() != ecPedersenCommitmentLength) + if (ctx.tx[sfBalanceCommitment].size() != ecPedersenCommitmentLength) return temMALFORMED; // check encrypted amount format after the above basic checks @@ -90,11 +90,11 @@ verifyProofs( // verify el gamal pedersen linkage { Buffer const pedersen{ptr, ecPedersenProofLength}; - if (auto const ter = verifyPedersenLinkage( + if (auto const ter = verifyBalancePcmLinkage( pedersen, (*mptoken)[sfConfidentialBalanceSpending], holderPubKey, - tx[sfPedersenCommitment], + tx[sfBalanceCommitment], contextHash); !isTesSuccess(ter)) { diff --git a/src/xrpld/app/tx/detail/ConfidentialSend.cpp b/src/xrpld/app/tx/detail/ConfidentialSend.cpp index 5b65883365..5bc315d1a9 100644 --- a/src/xrpld/app/tx/detail/ConfidentialSend.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialSend.cpp @@ -45,8 +45,16 @@ ConfidentialSend::preflight(PreflightContext const& ctx) // Check the length of the ZKProof auto const recipientCount = getConfidentialRecipientCount(hasAuditor); - if (ctx.tx[sfZKProof].length() != - getMultiCiphertextEqualityProofSize(recipientCount)) + auto const sizeEquality = + getMultiCiphertextEqualityProofSize(recipientCount); + auto const sizePedersenLinkage = 2 * ecPedersenProofLength; + + if (ctx.tx[sfZKProof].length() != sizeEquality + sizePedersenLinkage) + return temMALFORMED; + + // Check the length of Pedersen commitments + if (ctx.tx[sfBalanceCommitment].size() != ecPedersenCommitmentLength || + ctx.tx[sfAmountCommitment].size() != ecPedersenCommitmentLength) return temMALFORMED; // Check the encrypted amount formats, this is more expensive so put it at @@ -62,6 +70,125 @@ ConfidentialSend::preflight(PreflightContext const& ctx) return tesSUCCESS; } +TER +verifySendProofs( + PreclaimContext const& ctx, + std::shared_ptr const& sleSenderMPToken, + std::shared_ptr const& sleDestinationMPToken, + std::shared_ptr const& sleIssuance) +{ + // Sanity check + if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); + auto const recipientCount = getConfidentialRecipientCount(hasAuditor); + auto const proof = ctx.tx[sfZKProof]; + size_t remainingLength = proof.size(); + size_t currentOffset = 0; + + // Extract equality proof + auto const sizeEquality = + getMultiCiphertextEqualityProofSize(recipientCount); + if (remainingLength < sizeEquality) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const equalityProof = proof.substr(currentOffset, sizeEquality); + currentOffset += sizeEquality; + remainingLength -= sizeEquality; + + // Extract Pedersen linkage proof for amount commitment + if (remainingLength < ecPedersenProofLength) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const amountLinkageProof = + proof.substr(currentOffset, ecPedersenProofLength); + currentOffset += ecPedersenProofLength; + remainingLength -= ecPedersenProofLength; + + // Extract Pedersen linkage proof for balance commitment + if (remainingLength < ecPedersenProofLength) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const balanceLinkageProof = + proof.substr(currentOffset, ecPedersenProofLength); + currentOffset += ecPedersenProofLength; + remainingLength -= ecPedersenProofLength; + + // todo: Extract range proof once the lib is ready + if (remainingLength != 0) + return tecINTERNAL; // LCOV_EXCL_LINE + + // Prepare receipient list + std::vector recipients; + recipients.reserve(recipientCount); + + recipients.push_back( + {(*sleSenderMPToken)[sfHolderElGamalPublicKey], + ctx.tx[sfSenderEncryptedAmount]}); + + recipients.push_back( + {(*sleDestinationMPToken)[sfHolderElGamalPublicKey], + ctx.tx[sfDestinationEncryptedAmount]}); + + recipients.push_back( + {(*sleIssuance)[sfIssuerElGamalPublicKey], + ctx.tx[sfIssuerEncryptedAmount]}); + + if (hasAuditor) + { + recipients.push_back( + {(*sleIssuance)[sfAuditorElGamalPublicKey], + ctx.tx[sfAuditorEncryptedAmount]}); + } + + // Prepare the context hash + auto const contextHash = getSendContextHash( + ctx.tx[sfAccount], + ctx.tx[sfSequence], + ctx.tx[sfMPTokenIssuanceID], + ctx.tx[sfDestination], + (*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0)); + + // Verify the multi-ciphertext equality proof + if (auto const ter = verifyMultiCiphertextEqualityProof( + equalityProof, recipients, recipientCount, contextHash); + !isTesSuccess(ter)) + { + JLOG(ctx.j.trace()) << "ConfidentialSend: Equality proof failed."; + return ter; + } + + // Verify amount linkage + if (auto const ter = verifyAmountPcmLinkage( + amountLinkageProof, + ctx.tx[sfSenderEncryptedAmount], + (*sleSenderMPToken)[sfHolderElGamalPublicKey], + ctx.tx[sfAmountCommitment], + contextHash); + !isTesSuccess(ter)) + { + JLOG(ctx.j.trace()) << "ConfidentialSend: Amount linkage proof failed."; + return ter; + } + + // Verify balance linkage + if (auto const ter = verifyBalancePcmLinkage( + balanceLinkageProof, + (*sleSenderMPToken)[sfConfidentialBalanceSpending], + (*sleSenderMPToken)[sfHolderElGamalPublicKey], + ctx.tx[sfBalanceCommitment], + contextHash); + !isTesSuccess(ter)) + { + JLOG(ctx.j.trace()) + << "ConfidentialSend: Balance linkage proof failed."; + return ter; + } + + return tesSUCCESS; +} + TER ConfidentialSend::preclaim(PreclaimContext const& ctx) { @@ -155,52 +282,8 @@ ConfidentialSend::preclaim(PreclaimContext const& ctx) !isTesSuccess(ter)) return ter; - auto const contextHash = getSendContextHash( - account, - ctx.tx[sfSequence], - mptIssuanceID, - destination, - (*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0)); - - auto const expectedRecipients = - getConfidentialRecipientCount(requiresAuditor); - - // Prepare encrypted amount info for proof verification - std::vector recipients; - recipients.reserve(expectedRecipients); - - // Add sender encrypted amount info - recipients.push_back( - {(*sleSenderMPToken)[sfHolderElGamalPublicKey], - ctx.tx[sfSenderEncryptedAmount]}); - - // Add destination encrypted amount info - recipients.push_back( - {(*sleDestinationMPToken)[sfHolderElGamalPublicKey], - ctx.tx[sfDestinationEncryptedAmount]}); - - // Add issuer encrypted amount info - recipients.push_back( - {(*sleIssuance)[sfIssuerElGamalPublicKey], - ctx.tx[sfIssuerEncryptedAmount]}); - - // Add auditor encrypted amount info if present - if (requiresAuditor) - { - recipients.push_back( - {(*sleIssuance)[sfAuditorElGamalPublicKey], - ctx.tx[sfAuditorEncryptedAmount]}); - } - - // Verify the multi-ciphertext equality proof - if (auto const ter = verifyMultiCiphertextEqualityProof( - ctx.tx[sfZKProof], recipients, expectedRecipients, contextHash); - !isTesSuccess(ter)) - { - return ter; - } - - return tesSUCCESS; + return verifySendProofs( + ctx, sleSenderMPToken, sleDestinationMPToken, sleIssuance); } TER @@ -209,13 +292,14 @@ ConfidentialSend::doApply() auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; auto const destination = ctx_.tx[sfDestination]; - auto sleSender = view().peek(keylet::mptoken(mptIssuanceID, account_)); - auto sleDestination = + auto sleSenderMPToken = + view().peek(keylet::mptoken(mptIssuanceID, account_)); + auto sleDestinationMPToken = view().peek(keylet::mptoken(mptIssuanceID, destination)); auto sleDestAcct = view().peek(keylet::account(destination)); - if (!sleSender || !sleDestination || !sleDestAcct) + if (!sleSenderMPToken || !sleDestinationMPToken || !sleDestAcct) return tecINTERNAL; if (auto err = verifyDepositPreauth( @@ -236,7 +320,8 @@ ConfidentialSend::doApply() // Subtract from sender's spending balance { - Slice const curSpending = (*sleSender)[sfConfidentialBalanceSpending]; + Slice const curSpending = + (*sleSenderMPToken)[sfConfidentialBalanceSpending]; Buffer newSpending(ecGamalEncryptedTotalLength); if (TER const ter = @@ -244,12 +329,13 @@ ConfidentialSend::doApply() !isTesSuccess(ter)) return tecINTERNAL; - (*sleSender)[sfConfidentialBalanceSpending] = newSpending; + (*sleSenderMPToken)[sfConfidentialBalanceSpending] = newSpending; } // Subtract from issuer's balance { - Slice const curIssuerEnc = (*sleSender)[sfIssuerEncryptedBalance]; + Slice const curIssuerEnc = + (*sleSenderMPToken)[sfIssuerEncryptedBalance]; Buffer newIssuerEnc(ecGamalEncryptedTotalLength); if (TER const ter = @@ -257,13 +343,14 @@ ConfidentialSend::doApply() !isTesSuccess(ter)) return tecINTERNAL; - (*sleSender)[sfIssuerEncryptedBalance] = newIssuerEnc; + (*sleSenderMPToken)[sfIssuerEncryptedBalance] = newIssuerEnc; } // Subtract from auditor's balance if present if (auditorEc) { - Slice const curAuditorEnc = (*sleSender)[sfAuditorEncryptedBalance]; + Slice const curAuditorEnc = + (*sleSenderMPToken)[sfAuditorEncryptedBalance]; Buffer newAuditorEnc(ecGamalEncryptedTotalLength); if (TER const ter = @@ -271,24 +358,26 @@ ConfidentialSend::doApply() !isTesSuccess(ter)) return tecINTERNAL; - (*sleSender)[sfAuditorEncryptedBalance] = newAuditorEnc; + (*sleSenderMPToken)[sfAuditorEncryptedBalance] = newAuditorEnc; } // Add to destination's inbox balance { - Slice const curInbox = (*sleDestination)[sfConfidentialBalanceInbox]; + Slice const curInbox = + (*sleDestinationMPToken)[sfConfidentialBalanceInbox]; Buffer newInbox(ecGamalEncryptedTotalLength); if (TER const ter = homomorphicAdd(curInbox, destEc, newInbox); !isTesSuccess(ter)) return tecINTERNAL; - (*sleDestination)[sfConfidentialBalanceInbox] = newInbox; + (*sleDestinationMPToken)[sfConfidentialBalanceInbox] = newInbox; } // Add to issuer's balance { - Slice const curIssuerEnc = (*sleDestination)[sfIssuerEncryptedBalance]; + Slice const curIssuerEnc = + (*sleDestinationMPToken)[sfIssuerEncryptedBalance]; Buffer newIssuerEnc(ecGamalEncryptedTotalLength); if (TER const ter = @@ -296,14 +385,14 @@ ConfidentialSend::doApply() !isTesSuccess(ter)) return tecINTERNAL; - (*sleDestination)[sfIssuerEncryptedBalance] = newIssuerEnc; + (*sleDestinationMPToken)[sfIssuerEncryptedBalance] = newIssuerEnc; } // Add to auditor's balance if present if (auditorEc) { Slice const curAuditorEnc = - (*sleDestination)[sfAuditorEncryptedBalance]; + (*sleDestinationMPToken)[sfAuditorEncryptedBalance]; Buffer newAuditorEnc(ecGamalEncryptedTotalLength); if (TER const ter = @@ -311,15 +400,15 @@ ConfidentialSend::doApply() !isTesSuccess(ter)) return tecINTERNAL; - (*sleDestination)[sfAuditorEncryptedBalance] = newAuditorEnc; + (*sleDestinationMPToken)[sfAuditorEncryptedBalance] = newAuditorEnc; } // increment version - incrementConfidentialVersion(*sleSender); - incrementConfidentialVersion(*sleDestination); + incrementConfidentialVersion(*sleSenderMPToken); + incrementConfidentialVersion(*sleDestinationMPToken); - view().update(sleSender); - view().update(sleDestination); + view().update(sleSenderMPToken); + view().update(sleDestinationMPToken); return tesSUCCESS; } } // namespace ripple