From 1297385b7e8bd4711cbbe71e8161b7159d26ab7f Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Mon, 26 Jan 2026 12:39:35 -0500 Subject: [PATCH] Support ConfidentialSend equality proof (#6274) * Support ConfidentialSend equality proof * resolve conflicts * Add version check in send --- include/xrpl/protocol/ConfidentialTransfer.h | 110 +++- include/xrpl/protocol/Protocol.h | 3 + src/libxrpl/protocol/ConfidentialTransfer.cpp | 399 +++++++++++- src/test/app/ConfidentialTransfer_test.cpp | 442 ++++++------- src/test/jtx/impl/mpt.cpp | 613 ++++++++++++------ src/test/jtx/mpt.h | 26 +- .../app/tx/detail/ConfidentialConvert.cpp | 5 +- .../app/tx/detail/ConfidentialConvertBack.cpp | 16 +- src/xrpld/app/tx/detail/ConfidentialSend.cpp | 65 +- 9 files changed, 1187 insertions(+), 492 deletions(-) diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index 943806cd71..94f8125f9e 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -16,15 +16,38 @@ #include namespace ripple { +// Helper struct to bundle the ElGamal Public Key and the associated Ciphertext +struct ConfidentialRecipient +{ + Slice const publicKey; + Slice const encryptedAmount; +}; + +inline void +incrementConfidentialVersion(STObject& mptoken) +{ + // Retrieve current version and increment. + // Unsigned integer overflow is defined behavior in C++ (wraps to 0), + // which is acceptable here. + mptoken[sfConfidentialBalanceVersion] = + mptoken[~sfConfidentialBalanceVersion].value_or(0u) + 1u; +} void addCommonZKPFields( Serializer& s, std::uint16_t txType, + AccountID const& account, + std::uint32_t sequence, + uint192 const& issuanceID); + +uint256 +getSendContextHash( AccountID const& account, std::uint32_t sequence, uint192 const& issuanceID, - std::uint64_t amount); + AccountID const& destination, + std::uint32_t version); uint256 getClawbackContextHash( @@ -101,6 +124,33 @@ verifyElGamalEncryption( Slice const& pubKeySlice, Slice const& ciphertext); +NotTEC +checkEncryptedAmountFormat(STObject const& object); + +TER +verifyRevealedAmount( + std::uint64_t const amount, + Slice const& blindingFactor, + ConfidentialRecipient const& holder, + ConfidentialRecipient const& issuer, + std::optional const& auditor); + +constexpr std::size_t +getConfidentialRecipientCount(bool hasAuditor) +{ + return hasAuditor ? 4 : 3; +} + +std::size_t +getMultiCiphertextEqualityProofSize(std::size_t nRecipients); + +TER +verifyMultiCiphertextEqualityProof( + Slice const& proof, + std::vector const& recipients, + std::size_t const nRecipients, + uint256 const& contextHash); + TER verifyClawbackEqualityProof( uint64_t const amount, @@ -109,24 +159,6 @@ verifyClawbackEqualityProof( Slice const& ciphertext, uint256 const& contextHash); -NotTEC -checkEncryptedAmountFormat(STObject const& object); - -// Helper struct to bundle the ElGamal Public Key and the associated Ciphertext -struct EncryptedAmountInfo -{ - Slice const publicKey; - Slice const encryptedAmount; -}; - -TER -verifyRevealedAmount( - std::uint64_t const amount, - Slice const& blindingFactor, - EncryptedAmountInfo const& holder, - EncryptedAmountInfo const& issuer, - std::optional const& auditor); - // generates a 32 byte randomness factor to be used in encryption and proofs Buffer generateBlindingFactor(); @@ -392,6 +424,46 @@ secp256k1_mpt_pedersen_commit( uint64_t amount, unsigned char const* blinding_factor_rho /* 32 bytes */ ); + +// Multi-proof for same plaintexts +void +build_hash_input( + unsigned char* hash_out, // Output: 32-byte hash + size_t n, + secp256k1_pubkey const* R, + secp256k1_pubkey const* S, + secp256k1_pubkey const* Pk, + secp256k1_pubkey const* T_m, + secp256k1_pubkey const* T_rG, + secp256k1_pubkey const* T_rP, + unsigned char const* tx_id); + +size_t +secp256k1_mpt_prove_same_plaintext_multi_size(size_t n); + +int +secp256k1_mpt_prove_same_plaintext_multi( + secp256k1_context const* ctx, + unsigned char* proof_out, + size_t* proof_len, + uint64_t amount_m, + size_t n, + secp256k1_pubkey const* R, + secp256k1_pubkey const* S, + secp256k1_pubkey const* Pk, + unsigned char const* r_array, + unsigned char const* tx_id); + +int +secp256k1_mpt_verify_same_plaintext_multi( + secp256k1_context const* ctx, + unsigned char const* proof, + size_t proof_len, + size_t n, + secp256k1_pubkey const* R, + secp256k1_pubkey const* S, + secp256k1_pubkey const* Pk, + unsigned char const* tx_id); } // namespace ripple #endif diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 2005c16550..2fd10403be 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -318,6 +318,9 @@ std::size_t constexpr ecBlindingFactorLength = 32; /** Length of Schnorr ZKProof for public key registration */ std::size_t constexpr ecSchnorrProofLength = 65; +/** Length of ElGamal ciphertext equality proof */ +std::size_t constexpr ecCiphertextEqualityProofLength = 261; + /** Length of ElGamal Pedersen linkage proof */ std::size_t constexpr ecPedersenProofLength = 195; diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index 851ef624ee..1a3e1e109f 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -11,14 +11,29 @@ addCommonZKPFields( std::uint16_t txType, AccountID const& account, std::uint32_t sequence, - uint192 const& issuanceID, - std::uint64_t amount) + uint192 const& issuanceID) { s.add16(txType); s.addBitString(account); s.add32(sequence); s.addBitString(issuanceID); - s.add64(amount); +} + +uint256 +getSendContextHash( + AccountID const& account, + std::uint32_t sequence, + uint192 const& issuanceID, + AccountID const& destination, + std::uint32_t version) +{ + Serializer s; + addCommonZKPFields(s, ttCONFIDENTIAL_SEND, account, sequence, issuanceID); + + s.addBitString(destination); + s.addInteger(version); + + return s.getSHA512Half(); } uint256 @@ -31,8 +46,9 @@ getClawbackContextHash( { Serializer s; addCommonZKPFields( - s, ttCONFIDENTIAL_CLAWBACK, account, sequence, issuanceID, amount); + s, ttCONFIDENTIAL_CLAWBACK, account, sequence, issuanceID); + s.add64(amount); s.addBitString(holder); return s.getSHA512Half(); @@ -47,7 +63,9 @@ getConvertContextHash( { Serializer s; addCommonZKPFields( - s, ttCONFIDENTIAL_CONVERT, account, sequence, issuanceID, amount); + s, ttCONFIDENTIAL_CONVERT, account, sequence, issuanceID); + + s.add64(amount); return s.getSHA512Half(); } @@ -62,8 +80,9 @@ getConvertBackContextHash( { Serializer s; addCommonZKPFields( - s, ttCONFIDENTIAL_CONVERT_BACK, account, sequence, issuanceID, amount); + s, ttCONFIDENTIAL_CONVERT_BACK, account, sequence, issuanceID); + s.add64(amount); s.addInteger(version); return s.getSHA512Half(); @@ -314,9 +333,9 @@ TER verifyRevealedAmount( std::uint64_t const amount, Slice const& blindingFactor, - EncryptedAmountInfo const& holder, - EncryptedAmountInfo const& issuer, - std::optional const& auditor) + ConfidentialRecipient const& holder, + ConfidentialRecipient const& issuer, + std::optional const& auditor) { if (auto const res = verifyElGamalEncryption( amount, blindingFactor, holder.publicKey, holder.encryptedAmount); @@ -348,6 +367,63 @@ verifyRevealedAmount( return tesSUCCESS; } +std::size_t +getMultiCiphertextEqualityProofSize(std::size_t nRecipients) +{ + // Points (33 bytes): T_m (1) + T_rG (nRecipients) + T_rP (nRecipients) = 1 + // + 2nRecipients Scalars (32 bytes): s_m (1) + s_r (nRecipients) = 1 + + // nRecipients + return ((1 + (2 * nRecipients)) * 33) + ((1 + nRecipients) * 32); +} + +TER +verifyMultiCiphertextEqualityProof( + Slice const& proof, + std::vector const& recipients, + std::size_t const nRecipients, + uint256 const& contextHash) +{ + if (recipients.size() != nRecipients) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (proof.size() != getMultiCiphertextEqualityProofSize(nRecipients)) + return tecINTERNAL; // LCOV_EXCL_LINE + + std::vector r(nRecipients); + std::vector s(nRecipients); + std::vector pk(nRecipients); + + for (size_t i = 0; i < nRecipients; ++i) + { + auto const& recipient = recipients[i]; + if (recipient.encryptedAmount.size() != ecGamalEncryptedTotalLength) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!makeEcPair(recipient.encryptedAmount, r[i], s[i])) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (recipient.publicKey.size() != ecPubKeyLength) + return tecINTERNAL; // LCOV_EXCL_LINE + + std::memcpy(pk[i].data, recipient.publicKey.data(), ecPubKeyLength); + } + + int const result = secp256k1_mpt_verify_same_plaintext_multi( + secp256k1Context(), + proof.data(), + proof.size(), + nRecipients, + r.data(), + s.data(), + pk.data(), + contextHash.data()); + + if (result != 1) + return tecBAD_PROOF; + + return tesSUCCESS; +} + TER verifyClawbackEqualityProof( uint64_t const amount, @@ -1532,4 +1608,309 @@ secp256k1_mpt_pedersen_commit( return 1; } +// Multi-proof for same plaintexts +/** + * Builds the challenge hash: Domain || PublicInputs || Commitments || TxID + */ +void +build_hash_input( + unsigned char* hash_out, // Output: 32-byte hash + size_t n, + secp256k1_pubkey const* R, + secp256k1_pubkey const* S, + secp256k1_pubkey const* Pk, + secp256k1_pubkey const* T_m, + secp256k1_pubkey const* T_rG, + secp256k1_pubkey const* T_rP, + unsigned char const* tx_id) +{ + SHA256_CTX sha_ctx; + char const* domain = "MPT_POK_SAME_PLAINTEXT_PROOF"; + unsigned char buf[33]; + size_t len = 33; + size_t i; + secp256k1_context* ser_ctx = + secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + SHA256_Init(&sha_ctx); + SHA256_Update(&sha_ctx, domain, strlen(domain)); + + // Public Inputs (R, S, Pk for each ciphertext) + for (i = 0; i < n; ++i) + { + secp256k1_ec_pubkey_serialize( + ser_ctx, buf, &len, &R[i], SECP256K1_EC_COMPRESSED); + SHA256_Update(&sha_ctx, buf, 33); + secp256k1_ec_pubkey_serialize( + ser_ctx, buf, &len, &S[i], SECP256K1_EC_COMPRESSED); + SHA256_Update(&sha_ctx, buf, 33); + secp256k1_ec_pubkey_serialize( + ser_ctx, buf, &len, &Pk[i], SECP256K1_EC_COMPRESSED); + SHA256_Update(&sha_ctx, buf, 33); + } + + // Commitments + secp256k1_ec_pubkey_serialize( + ser_ctx, buf, &len, T_m, SECP256K1_EC_COMPRESSED); + SHA256_Update(&sha_ctx, buf, 33); + + for (i = 0; i < n; ++i) + { + secp256k1_ec_pubkey_serialize( + ser_ctx, buf, &len, &T_rG[i], SECP256K1_EC_COMPRESSED); + SHA256_Update(&sha_ctx, buf, 33); + secp256k1_ec_pubkey_serialize( + ser_ctx, buf, &len, &T_rP[i], SECP256K1_EC_COMPRESSED); + SHA256_Update(&sha_ctx, buf, 33); + } + + SHA256_Update(&sha_ctx, tx_id, 32); + SHA256_Final(hash_out, &sha_ctx); + secp256k1_context_destroy(ser_ctx); +} + +/* --- Public API --- */ + +size_t +secp256k1_mpt_prove_same_plaintext_multi_size(size_t n) +{ + // (1 point T_m + 2*N points T_r) * 33 + (1 scalar s_m + N scalars s_r) * 32 + return ((1 + 2 * n) * 33) + ((1 + n) * 32); +} + +int +secp256k1_mpt_prove_same_plaintext_multi( + secp256k1_context const* ctx, + unsigned char* proof_out, + size_t* proof_len, + uint64_t amount_m, + size_t n, + secp256k1_pubkey const* R, + secp256k1_pubkey const* S, + secp256k1_pubkey const* Pk, + unsigned char const* r_array, + unsigned char const* tx_id) +{ + size_t required_len = secp256k1_mpt_prove_same_plaintext_multi_size(n); + if (*proof_len < required_len) + { + *proof_len = required_len; + return 0; + } + *proof_len = required_len; + + unsigned char k_m[32]; + unsigned char k_r[n][32]; + secp256k1_pubkey T_m; + secp256k1_pubkey T_rG[n]; + secp256k1_pubkey T_rP[n]; + unsigned char e[32]; + unsigned char s_m[32]; + unsigned char s_r[n][32]; + + size_t i; + int ok = 1; + + /* 1. Generate Randomness & Commitments */ + if (!generate_random_scalar(ctx, k_m)) + return 0; + if (!secp256k1_ec_pubkey_create(ctx, &T_m, k_m)) + ok = 0; + + for (i = 0; i < n; ++i) + { + if (!generate_random_scalar(ctx, k_r[i])) + ok = 0; + if (!secp256k1_ec_pubkey_create(ctx, &T_rG[i], k_r[i])) + ok = 0; + + T_rP[i] = Pk[i]; + if (!secp256k1_ec_pubkey_tweak_mul(ctx, &T_rP[i], k_r[i])) + ok = 0; + } + + if (!ok) + return 0; + + /* 2. Compute Challenge e */ + build_hash_input(e, n, R, S, Pk, &T_m, T_rG, T_rP, tx_id); + // Ensure e is valid + if (!secp256k1_ec_seckey_verify(ctx, e)) + return 0; + + /* 3. Compute Responses */ + /* s_m = k_m + e * m */ + unsigned char m_scalar[32] = {0}; + for (i = 0; i < 8; ++i) + m_scalar[31 - i] = (amount_m >> (i * 8)) & 0xFF; + + memcpy(s_m, k_m, 32); + unsigned char term[32]; + memcpy(term, m_scalar, 32); + if (!secp256k1_ec_seckey_tweak_mul(ctx, term, e)) + return 0; + if (!secp256k1_ec_seckey_tweak_add(ctx, s_m, term)) + return 0; + + /* s_ri = k_ri + e * ri */ + for (i = 0; i < n; ++i) + { + memcpy(s_r[i], k_r[i], 32); + memcpy(term, &r_array[i * 32], 32); // Extract r_i from flat array + if (!secp256k1_ec_seckey_tweak_mul(ctx, term, e)) + return 0; + if (!secp256k1_ec_seckey_tweak_add(ctx, s_r[i], term)) + return 0; + } + + /* 4. Serialize Proof */ + size_t offset = 0; + size_t len = 33; + + // Points + secp256k1_ec_pubkey_serialize( + ctx, proof_out + offset, &len, &T_m, SECP256K1_EC_COMPRESSED); + offset += 33; + for (i = 0; i < n; ++i) + { + secp256k1_ec_pubkey_serialize( + ctx, proof_out + offset, &len, &T_rG[i], SECP256K1_EC_COMPRESSED); + offset += 33; + } + for (i = 0; i < n; ++i) + { + secp256k1_ec_pubkey_serialize( + ctx, proof_out + offset, &len, &T_rP[i], SECP256K1_EC_COMPRESSED); + offset += 33; + } + + // Scalars + memcpy(proof_out + offset, s_m, 32); + offset += 32; + for (i = 0; i < n; ++i) + { + memcpy(proof_out + offset, s_r[i], 32); + offset += 32; + } + + return 1; +} + +int +secp256k1_mpt_verify_same_plaintext_multi( + secp256k1_context const* ctx, + unsigned char const* proof, + size_t proof_len, + size_t n, + secp256k1_pubkey const* R, + secp256k1_pubkey const* S, + secp256k1_pubkey const* Pk, + unsigned char const* tx_id) +{ + if (proof_len != secp256k1_mpt_prove_same_plaintext_multi_size(n)) + return 0; + + /* Deserialize */ + size_t offset = 0; + secp256k1_pubkey T_m; + secp256k1_pubkey T_rG[n]; + secp256k1_pubkey T_rP[n]; + unsigned char s_m[32]; + unsigned char s_r[n][32]; + size_t i; + + if (!secp256k1_ec_pubkey_parse(ctx, &T_m, proof + offset, 33)) + return 0; + offset += 33; + for (i = 0; i < n; ++i) + { + if (!secp256k1_ec_pubkey_parse(ctx, &T_rG[i], proof + offset, 33)) + return 0; + offset += 33; + } + for (i = 0; i < n; ++i) + { + if (!secp256k1_ec_pubkey_parse(ctx, &T_rP[i], proof + offset, 33)) + return 0; + offset += 33; + } + + memcpy(s_m, proof + offset, 32); + offset += 32; + for (i = 0; i < n; ++i) + { + memcpy(s_r[i], proof + offset, 32); + offset += 32; + } + + /* Recompute Challenge */ + unsigned char e[32]; + build_hash_input(e, n, R, S, Pk, &T_m, T_rG, T_rP, tx_id); + + /* Verify Equations */ + secp256k1_pubkey lhs, rhs, term, SmG; + secp256k1_pubkey const* add_pt[3]; + unsigned char b1[33], b2[33]; + size_t len; + + // Precompute s_m * G + if (!secp256k1_ec_pubkey_create(ctx, &SmG, s_m)) + return 0; + + for (i = 0; i < n; ++i) + { + /* Check 1: s_ri * G == T_ri_G + e * R_i */ + if (!secp256k1_ec_pubkey_create(ctx, &lhs, s_r[i])) + return 0; + + term = R[i]; + if (!secp256k1_ec_pubkey_tweak_mul(ctx, &term, e)) + return 0; + add_pt[0] = &T_rG[i]; + add_pt[1] = &term; + if (!secp256k1_ec_pubkey_combine(ctx, &rhs, add_pt, 2)) + return 0; + + len = 33; + secp256k1_ec_pubkey_serialize( + ctx, b1, &len, &lhs, SECP256K1_EC_COMPRESSED); + len = 33; + secp256k1_ec_pubkey_serialize( + ctx, b2, &len, &rhs, SECP256K1_EC_COMPRESSED); + if (memcmp(b1, b2, 33) != 0) + return 0; + + /* Check 2: s_m * G + s_ri * P_i == T_m + T_ri_P + e * S_i */ + /* LHS = SmG + s_r[i] * Pk[i] */ + term = Pk[i]; + if (!secp256k1_ec_pubkey_tweak_mul(ctx, &term, s_r[i])) + return 0; + add_pt[0] = &SmG; + add_pt[1] = &term; + if (!secp256k1_ec_pubkey_combine(ctx, &lhs, add_pt, 2)) + return 0; + + /* RHS = T_m + T_rP[i] + e * S[i] */ + term = S[i]; + if (!secp256k1_ec_pubkey_tweak_mul(ctx, &term, e)) + return 0; + add_pt[0] = &T_m; + add_pt[1] = &T_rP[i]; + add_pt[2] = &term; + if (!secp256k1_ec_pubkey_combine(ctx, &rhs, add_pt, 3)) + return 0; + + len = 33; + secp256k1_ec_pubkey_serialize( + ctx, b1, &len, &lhs, SECP256K1_EC_COMPRESSED); + len = 33; + secp256k1_ec_pubkey_serialize( + ctx, b2, &len, &rhs, SECP256K1_EC_COMPRESSED); + if (memcmp(b1, b2, 33) != 0) + return 0; + } + + return 1; +} + } // namespace ripple diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index e526f8e09f..22b971596f 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -9,14 +9,44 @@ namespace ripple { class ConfidentialTransfer_test : public beast::unit_test::suite { - // A 66-byte array of random unsigned char values - constexpr static unsigned char badCiphertext[ecGamalEncryptedTotalLength] = - {0x3E, 0x9A, 0x0F, 0x7C, 0x51, 0xD8, 0x22, 0x8B, 0x6E, 0x14, 0xC9, - 0xF5, 0x4D, 0x6A, 0x03, 0x81, 0x77, 0x2B, 0xEE, 0x9F, 0x10, 0xC2, - 0x57, 0x3D, 0x88, 0x65, 0x0C, 0xAB, 0xF1, 0x4E, 0x19, 0x96, 0x2A, - 0x73, 0xDC, 0x44, 0xB8, 0x5F, 0x01, 0xEA, 0x87, 0x36, 0x60, 0xCE, - 0x92, 0x25, 0x7D, 0x5B, 0xC0, 0x1E, 0x48, 0xF9, 0x84, 0x33, 0x67, - 0xAD, 0x0B, 0xE3, 0x91, 0x50, 0xDA, 0x2F, 0x75, 0xC6, 0xBD, 0x42}; + // Get a bad ciphertext with valid structure but cryptographic invalid for + // testing purposes. For preflight test purposes. + static Buffer const& + getBadCiphertext() + { + static Buffer const badCiphertext = []() { + Buffer buf(ecGamalEncryptedTotalLength); + std::memset(buf.data(), 0xFF, ecGamalEncryptedTotalLength); + + buf.data()[0] = 0x02; + buf.data()[ecGamalEncryptedLength] = 0x02; + return buf; + }(); + + return badCiphertext; + } + + // Get a trivial buffer that is structurally and mathematically valid, but + // contains invalid data that does not match the ledger state. For preclaim + // test purposes. + static Buffer const& + getTrivialCiphertext() + { + static Buffer const trivialCiphertext = []() { + Buffer buf(ecGamalEncryptedTotalLength); + std::memset(buf.data(), 0, ecGamalEncryptedTotalLength); + + buf.data()[0] = 0x02; + buf.data()[ecGamalEncryptedLength] = 0x02; + + buf.data()[ecGamalEncryptedLength - 1] = 0x01; + buf.data()[ecGamalEncryptedTotalLength - 1] = 0x01; + + return buf; + }(); + + return trivialCiphertext; + } void testConvert(FeatureBitset features) @@ -203,16 +233,14 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .amt = 1, .holderPubKey = mptAlice.getPubKey(bob), - .holderEncryptedAmt = - Buffer{badCiphertext, ecGamalEncryptedTotalLength}, + .holderEncryptedAmt = getBadCiphertext(), .err = temBAD_CIPHERTEXT}); mptAlice.convert( {.account = bob, .amt = 1, .holderPubKey = mptAlice.getPubKey(bob), - .issuerEncryptedAmt = - Buffer{badCiphertext, ecGamalEncryptedTotalLength}, + .issuerEncryptedAmt = getBadCiphertext(), .err = temBAD_CIPHERTEXT}); // invalid pub key @@ -845,7 +873,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.create( {.ownerCount = 1, - .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy}); mptAlice.authorize({.account = bob}); @@ -865,41 +892,18 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.convert( {.account = bob, .amt = 60, - .holderPubKey = mptAlice.getPubKey(bob), - .err = tesSUCCESS}); - - BEAST_EXPECT(mptAlice.getBalance(bob) == 40); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - bob, MPTTester::HOLDER_ENCRYPTED_INBOX) == 60); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - bob, MPTTester::HOLDER_ENCRYPTED_SPENDING) == 0); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - bob, MPTTester::ISSUER_ENCRYPTED_BALANCE) == 60); + .holderPubKey = mptAlice.getPubKey(bob)}); // bob merge inbox mptAlice.mergeInbox({ .account = bob, }); + // carol convert 20 to confidential mptAlice.convert( {.account = carol, .amt = 20, - .holderPubKey = mptAlice.getPubKey(carol), - .err = tesSUCCESS}); - - BEAST_EXPECT(mptAlice.getBalance(carol) == 30); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - carol, MPTTester::HOLDER_ENCRYPTED_INBOX) == 20); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - carol, MPTTester::HOLDER_ENCRYPTED_SPENDING) == 0); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - carol, MPTTester::ISSUER_ENCRYPTED_BALANCE) == 20); + .holderPubKey = mptAlice.getPubKey(carol)}); // carol merge inbox mptAlice.mergeInbox({ @@ -907,32 +911,29 @@ class ConfidentialTransfer_test : public beast::unit_test::suite }); // bob sends 10 to carol - mptAlice.send( - {.account = bob, - .dest = carol, - .amt = 10, // will be encrypted internally - .proof = "123", - .err = tesSUCCESS}); + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + }); // bob sends 1 to carol again - mptAlice.send( - {.account = bob, - .dest = carol, - .amt = 1, - .proof = "123", - .err = tesSUCCESS}); + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 1, + }); mptAlice.mergeInbox({ .account = carol, }); // carol sends 15 backto bob - mptAlice.send( - {.account = carol, - .dest = bob, - .amt = 15, - .proof = "123", - .err = tesSUCCESS}); + mptAlice.send({ + .account = carol, + .dest = bob, + .amt = 15, + }); } void @@ -950,7 +951,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.create( {.ownerCount = 1, - .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy}); mptAlice.authorize({.account = bob}); @@ -973,22 +973,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.convert( {.account = bob, .amt = 60, - .holderPubKey = mptAlice.getPubKey(bob), - .err = tesSUCCESS}); - - BEAST_EXPECT(mptAlice.getBalance(bob) == 40); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - bob, MPTTester::HOLDER_ENCRYPTED_INBOX) == 60); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - bob, MPTTester::HOLDER_ENCRYPTED_SPENDING) == 0); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - bob, MPTTester::ISSUER_ENCRYPTED_BALANCE) == 60); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - bob, MPTTester::AUDITOR_ENCRYPTED_BALANCE) == 60); + .holderPubKey = mptAlice.getPubKey(bob)}); // bob merge inbox mptAlice.mergeInbox({ @@ -998,22 +983,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.convert( {.account = carol, .amt = 20, - .holderPubKey = mptAlice.getPubKey(carol), - .err = tesSUCCESS}); - - BEAST_EXPECT(mptAlice.getBalance(carol) == 30); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - carol, MPTTester::HOLDER_ENCRYPTED_INBOX) == 20); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - carol, MPTTester::HOLDER_ENCRYPTED_SPENDING) == 0); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - carol, MPTTester::ISSUER_ENCRYPTED_BALANCE) == 20); - BEAST_EXPECT( - mptAlice.getDecryptedBalance( - carol, MPTTester::AUDITOR_ENCRYPTED_BALANCE) == 20); + .holderPubKey = mptAlice.getPubKey(carol)}); // carol merge inbox mptAlice.mergeInbox({ @@ -1021,32 +991,17 @@ class ConfidentialTransfer_test : public beast::unit_test::suite }); // bob sends 10 to carol - mptAlice.send( - {.account = bob, - .dest = carol, - .amt = 10, // will be encrypted internally - .proof = "123", - .err = tesSUCCESS}); + mptAlice.send({.account = bob, .dest = carol, .amt = 10}); // bob sends 1 to carol again - mptAlice.send( - {.account = bob, - .dest = carol, - .amt = 1, - .proof = "123", - .err = tesSUCCESS}); + mptAlice.send({.account = bob, .dest = carol, .amt = 1}); mptAlice.mergeInbox({ .account = carol, }); // carol sends 15 backto bob - mptAlice.send( - {.account = carol, - .dest = bob, - .amt = 15, - .proof = "123", - .err = tesSUCCESS}); + mptAlice.send({.account = carol, .dest = bob, .amt = 15}); } void @@ -1071,7 +1026,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = carol, .amt = 10, - .proof = "123", .senderEncryptedAmt = Buffer(ripple::ecGamalEncryptedTotalLength), .destEncryptedAmt = @@ -1090,9 +1044,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( - {.ownerCount = 1, - .holderCount = 0, - .flags = tfMPTCanTransfer | tfMPTCanPrivacy}); + {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanPrivacy}); mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); @@ -1104,12 +1056,23 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.pay(alice, bob, 100); mptAlice.pay(alice, carol, 50); + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.convert({ + .account = carol, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(carol), + }); + // issuer can not be the same as sender mptAlice.send( {.account = alice, .dest = carol, .amt = 10, - .proof = "123", .senderEncryptedAmt = Buffer(ripple::ecGamalEncryptedTotalLength), .destEncryptedAmt = @@ -1123,7 +1086,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = bob, .amt = 10, - .proof = "123", .senderEncryptedAmt = Buffer(ripple::ecGamalEncryptedTotalLength), .destEncryptedAmt = @@ -1137,7 +1099,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = carol, .amt = 10, - .proof = "123", .senderEncryptedAmt = Buffer(10), .destEncryptedAmt = Buffer(ripple::ecGamalEncryptedTotalLength), @@ -1149,7 +1110,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = carol, .amt = 10, - .proof = "123", .senderEncryptedAmt = Buffer(ripple::ecGamalEncryptedTotalLength), .destEncryptedAmt = Buffer(10), @@ -1161,7 +1121,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = carol, .amt = 10, - .proof = "123", .senderEncryptedAmt = Buffer(ripple::ecGamalEncryptedTotalLength), .destEncryptedAmt = @@ -1169,43 +1128,46 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .issuerEncryptedAmt = Buffer(10), .err = temBAD_CIPHERTEXT}); - auto const ciphertextHex = generatePlaceholderCiphertext(); + // auto const ciphertextHex = generatePlaceholderCiphertext(); // sender encrypted amount malformed mptAlice.send( {.account = bob, .dest = carol, .amt = 10, - .proof = "123", .senderEncryptedAmt = Buffer(ripple::ecGamalEncryptedTotalLength), - .destEncryptedAmt = ciphertextHex, - .issuerEncryptedAmt = ciphertextHex, .err = temBAD_CIPHERTEXT}); // dest encrypted amount malformed mptAlice.send( {.account = bob, .dest = carol, .amt = 10, - .proof = "123", - .senderEncryptedAmt = ciphertextHex, .destEncryptedAmt = Buffer(ripple::ecGamalEncryptedTotalLength), - .issuerEncryptedAmt = ciphertextHex, .err = temBAD_CIPHERTEXT}); // issuer encrypted amount malformed mptAlice.send( {.account = bob, .dest = carol, .amt = 10, - .proof = "123", - .senderEncryptedAmt = ciphertextHex, - .destEncryptedAmt = ciphertextHex, .issuerEncryptedAmt = Buffer(ripple::ecGamalEncryptedTotalLength), .err = temBAD_CIPHERTEXT}); - // todo: proof length check + // invalid proof length + mptAlice.send( + {.account = bob, + .dest = carol, + .amt = 10, + .senderEncryptedAmt = + Buffer(ripple::ecGamalEncryptedTotalLength), + .destEncryptedAmt = + Buffer(ripple::ecGamalEncryptedTotalLength), + .issuerEncryptedAmt = + Buffer(ripple::ecGamalEncryptedTotalLength), + .proof = std::string(10, 'A'), + .err = temMALFORMED}); } } @@ -1265,8 +1227,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .account = carol, }); - auto const ciphertextHex = generatePlaceholderCiphertext(); - // issuance not found { Env env{*this, features}; @@ -1290,12 +1250,13 @@ class ConfidentialTransfer_test : public beast::unit_test::suite jv[jss::Destination] = carol.human(); jv[jss::TransactionType] = jss::ConfidentialSend; jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + jv[sfSenderEncryptedAmount] = strHex(getTrivialCiphertext()); + jv[sfDestinationEncryptedAmount] = strHex(getTrivialCiphertext()); + jv[sfIssuerEncryptedAmount] = strHex(getTrivialCiphertext()); - auto const encryptedAmt = strHex(ciphertextHex); - jv[sfSenderEncryptedAmount] = encryptedAmt; - jv[sfDestinationEncryptedAmount] = encryptedAmt; - jv[sfIssuerEncryptedAmount] = encryptedAmt; - jv[sfZKProof] = "123"; + auto const dummyProofSize = + secp256k1_mpt_prove_same_plaintext_multi_size(3); + jv[sfZKProof.jsonName] = strHex(Buffer(dummyProofSize)); env(jv, ter(tecOBJECT_NOT_FOUND)); } @@ -1307,9 +1268,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = unknown, .amt = 10, - .proof = "123", - .destEncryptedAmt = ciphertextHex, - .issuerEncryptedAmt = ciphertextHex, + .destEncryptedAmt = getTrivialCiphertext(), + .issuerEncryptedAmt = getTrivialCiphertext(), .err = tecNO_TARGET}); } @@ -1319,13 +1279,11 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = dave, .amt = 10, - .proof = "123", .err = tecNO_PERMISSION}); mptAlice.send( {.account = dave, .dest = carol, .amt = 10, - .proof = "123", .err = tecNO_PERMISSION}); } @@ -1335,8 +1293,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = eve, .amt = 10, - .proof = "123", - .destEncryptedAmt = ciphertextHex, + .destEncryptedAmt = getTrivialCiphertext(), .err = tecOBJECT_NOT_FOUND}); } @@ -1345,16 +1302,11 @@ class ConfidentialTransfer_test : public beast::unit_test::suite // lock issuance mptAlice.set({.account = alice, .flags = tfMPTLock}); mptAlice.send( - {.account = bob, - .dest = carol, - .amt = 10, - .proof = "123", - .err = tecLOCKED}); + {.account = bob, .dest = carol, .amt = 10, .err = tecLOCKED}); // unlock issuance mptAlice.set({.account = alice, .flags = tfMPTUnlock}); // now can send - mptAlice.send( - {.account = bob, .dest = carol, .amt = 1, .proof = "123"}); + mptAlice.send({.account = bob, .dest = carol, .amt = 1}); } // sender is locked @@ -1362,17 +1314,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite // lock bob mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); mptAlice.send( - {.account = bob, - .dest = carol, - .amt = 10, - .proof = "123", - .err = tecLOCKED}); + {.account = bob, .dest = carol, .amt = 10, .err = tecLOCKED}); // unlock bob mptAlice.set( {.account = alice, .holder = bob, .flags = tfMPTUnlock}); // now can send - mptAlice.send( - {.account = bob, .dest = carol, .amt = 2, .proof = "123"}); + mptAlice.send({.account = bob, .dest = carol, .amt = 2}); } // destination is locked @@ -1381,17 +1328,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.set( {.account = alice, .holder = carol, .flags = tfMPTLock}); mptAlice.send( - {.account = bob, - .dest = carol, - .amt = 10, - .proof = "123", - .err = tecLOCKED}); + {.account = bob, .dest = carol, .amt = 10, .err = tecLOCKED}); // unlock carol mptAlice.set( {.account = alice, .holder = carol, .flags = tfMPTUnlock}); // now can send - mptAlice.send( - {.account = bob, .dest = carol, .amt = 3, .proof = "123"}); + mptAlice.send({.account = bob, .dest = carol, .amt = 3}); } // sender not authorized @@ -1400,19 +1342,14 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.authorize( {.account = alice, .holder = bob, .flags = tfMPTUnauthorize}); mptAlice.send( - {.account = bob, - .dest = carol, - .amt = 10, - .proof = "123", - .err = tecNO_AUTH}); + {.account = bob, .dest = carol, .amt = 10, .err = tecNO_AUTH}); // authorize bob again mptAlice.authorize({ .account = alice, .holder = bob, }); // now can send - mptAlice.send( - {.account = bob, .dest = carol, .amt = 4, .proof = "123"}); + mptAlice.send({.account = bob, .dest = carol, .amt = 4}); } // destination not authorized @@ -1421,19 +1358,14 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.authorize( {.account = alice, .holder = carol, .flags = tfMPTUnauthorize}); mptAlice.send( - {.account = bob, - .dest = carol, - .amt = 10, - .proof = "123", - .err = tecNO_AUTH}); + {.account = bob, .dest = carol, .amt = 10, .err = tecNO_AUTH}); // authorize carol again mptAlice.authorize({ .account = alice, .holder = carol, }); // now can send - mptAlice.send( - {.account = bob, .dest = carol, .amt = 5, .proof = "123"}); + mptAlice.send({.account = bob, .dest = carol, .amt = 5}); } // cannot send when MPTCanTransfer is not set @@ -1445,9 +1377,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); mptAlice.create( - {.ownerCount = 1, - .holderCount = 0, - .flags = tfMPTCanLock | tfMPTCanPrivacy}); + {.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanPrivacy}); mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); @@ -1490,9 +1420,64 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = bob, .dest = carol, .amt = 10, // will be encrypted internally - .proof = "123", .err = tecNO_AUTH}); } + + // bad proof + { + 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 = 60, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tesSUCCESS}); + + mptAlice.mergeInbox({ + .account = bob, + }); + + mptAlice.convert( + {.account = carol, + .amt = 20, + .holderPubKey = mptAlice.getPubKey(carol), + .err = tesSUCCESS}); + + mptAlice.mergeInbox({ + .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)), + .err = tecBAD_PROOF}); + } } void @@ -1825,15 +1810,13 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.convertBack( {.account = bob, .amt = 30, - .holderEncryptedAmt = - Buffer{badCiphertext, ecGamalEncryptedTotalLength}, + .holderEncryptedAmt = getBadCiphertext(), .err = temBAD_CIPHERTEXT}); mptAlice.convertBack( {.account = bob, .amt = 30, - .issuerEncryptedAmt = - Buffer{badCiphertext, ecGamalEncryptedTotalLength}, + .issuerEncryptedAmt = getBadCiphertext(), .err = temBAD_CIPHERTEXT}); } } @@ -2054,10 +2037,10 @@ class ConfidentialTransfer_test : public beast::unit_test::suite using namespace std::chrono; - Account const alice("alice"); // issuer - Account const bob("bob"); // holder + Account const alice("alice"); + Account const bob("bob"); Account const carol("carol"); - Account const dpIssuer("dpIssuer"); // holder + Account const dpIssuer("dpIssuer"); env.fund(XRP(50000), dpIssuer); env.close(); @@ -2066,7 +2049,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.create( {.ownerCount = 1, - .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy}); mptAlice.authorize({.account = bob}); @@ -2086,16 +2068,16 @@ class ConfidentialTransfer_test : public beast::unit_test::suite env(fset(bob, asfDepositAuth)); env.close(); - mptAlice.convert( - {.account = carol, - .amt = 50, - .holderPubKey = mptAlice.getPubKey(carol), - .err = tesSUCCESS}); - mptAlice.convert( - {.account = bob, - .amt = 50, - .holderPubKey = mptAlice.getPubKey(bob), - .err = tesSUCCESS}); + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); // carol merge inbox mptAlice.mergeInbox({ @@ -2111,20 +2093,14 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.send( {.account = carol, .dest = bob, - .amt = 10, // will be encrypted internally - .proof = "123", + .amt = 10, .err = tecNO_PERMISSION}); // Bob authorize alice env(deposit::auth(bob, carol)); env.close(); - mptAlice.send({ - .account = carol, - .dest = bob, - .amt = 10, // will be encrypted internally - .proof = "123", - }); + mptAlice.send({.account = carol, .dest = bob, .amt = 10}); // Create credentials env(credentials::create(bob, dpIssuer, credType)); @@ -2137,8 +2113,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.send( {.account = carol, .dest = bob, - .amt = 10, // will be encrypted internally - .proof = "123", + .amt = 10, .credentials = {{credIdx}}}); // Bob revoke authorization @@ -2148,15 +2123,13 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.send( {.account = carol, .dest = bob, - .amt = 10, // will be encrypted internally - .proof = "123", + .amt = 10, .err = tecNO_PERMISSION}); mptAlice.send( {.account = carol, .dest = bob, - .amt = 10, // will be encrypted internally - .proof = "123", + .amt = 10, .credentials = {{credIdx}}, .err = tecNO_PERMISSION}); @@ -2167,15 +2140,13 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.send( {.account = carol, .dest = bob, - .amt = 10, // will be encrypted internally - .proof = "123", + .amt = 10, .err = tecNO_PERMISSION}); mptAlice.send({ .account = carol, .dest = bob, - .amt = 10, // will be encrypted internally - .proof = "123", + .amt = 10, .credentials = {{credIdx}}, }); } @@ -2252,8 +2223,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite // after send, bob's inbox balance is 50, spending balance // remains 60. carol's inbox balance remains 0, spending balance // drops to 70. - mptAlice.send( - {.account = carol, .dest = bob, .amt = 50, .proof = "123"}); + mptAlice.send({.account = carol, .dest = bob, .amt = 50}); // alice clawback all confidential balance from bob, 110 in total. // bob has balance in both inbox and spending. These balances should @@ -2350,8 +2320,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite // after send, bob's inbox balance is 50, spending balance // remains 60. carol's inbox balance remains 0, spending balance // drops to 70. - mptAlice.send( - {.account = carol, .dest = bob, .amt = 50, .proof = "123"}); + mptAlice.send({.account = carol, .dest = bob, .amt = 50}); // alice clawback all confidential balance from bob, 110 in total. // bob has balance in both inbox and spending. These balances should @@ -2406,7 +2375,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.create( {.ownerCount = 1, - .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy}); mptAlice.authorize({.account = bob}); @@ -2424,7 +2392,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = carol, .holder = bob, .amt = 10, - .proof = "123", .err = temMALFORMED}); // invalid issuance ID, whose issuer is not alice @@ -2448,7 +2415,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = alice, .holder = alice, .amt = 10, - .proof = "123", .err = temMALFORMED}); // invalid amount @@ -2456,10 +2422,9 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = alice, .holder = bob, .amt = 0, - .proof = "123", .err = temBAD_AMOUNT}); - // proof length invalid + // invalid proof length mptAlice.confidentialClaw( {.account = alice, .holder = bob, @@ -2801,10 +2766,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.mergeInbox({ .account = carol, }); - mptAlice.send( - {.account = bob, .dest = carol, .amt = 100, .proof = "123"}); - mptAlice.send( - {.account = carol, .dest = bob, .amt = 100, .proof = "123"}); + mptAlice.send({.account = bob, .dest = carol, .amt = 100}); + mptAlice.send({.account = carol, .dest = bob, .amt = 100}); // verify proof fails with invalid clawback amount // bob: 100 in inbox, 200 in spending @@ -3067,15 +3030,18 @@ class ConfidentialTransfer_test : public beast::unit_test::suite uint64_t const amt = 10; Buffer const blindingFactor = generateBlindingFactor(); Buffer const pcBlindingFactor = generateBlindingFactor(); - uint64_t const spendingBalance = mptAlice.getDecryptedBalance( + + auto const spendingBalance = mptAlice.getDecryptedBalance( bob, MPTTester::HOLDER_ENCRYPTED_SPENDING); + BEAST_EXPECT(spendingBalance.has_value()); auto const encryptedSpendingBalance = mptAlice.getEncryptedBalance( bob, MPTTester::HOLDER_ENCRYPTED_SPENDING); - - BEAST_EXPECT(encryptedSpendingBalance); + BEAST_EXPECT( + encryptedSpendingBalance.has_value() && + !encryptedSpendingBalance->empty()); Buffer const pedersenCommitment = - mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor); + mptAlice.getPedersenCommitment(*spendingBalance, pcBlindingFactor); Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor); Buffer const bobCiphertext = @@ -3099,7 +3065,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite { .pedersenCommitment = badPedersenCommitment, // bad pedersen commitment - .amt = spendingBalance, + .amt = *spendingBalance, .encryptedAmt = *encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); @@ -3135,7 +3101,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite blindingFactor, { .pedersenCommitment = pedersenCommitment, - .amt = spendingBalance, + .amt = *spendingBalance, .encryptedAmt = *encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); @@ -3170,7 +3136,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite blindingFactor, { .pedersenCommitment = pedersenCommitment, - .amt = spendingBalance, + .amt = *spendingBalance, .encryptedAmt = *encryptedSpendingBalance, .blindingFactor = generateBlindingFactor(), // bad blinding factor @@ -3204,7 +3170,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite blindingFactor, { .pedersenCommitment = pedersenCommitment, - .amt = spendingBalance, + .amt = *spendingBalance, .encryptedAmt = *encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 7459b1c7c6..884af1606b 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -14,21 +14,6 @@ namespace ripple { namespace test { namespace jtx { -ripple::Buffer -generatePlaceholderCiphertext() -{ - Buffer buf(ecGamalEncryptedTotalLength); - std::memset(buf.data(), 0, ecGamalEncryptedTotalLength); - - buf.data()[0] = 0x02; - buf.data()[ecGamalEncryptedLength] = 0x02; - - buf.data()[ecGamalEncryptedLength - 1] = 0x01; - buf.data()[ecGamalEncryptedTotalLength - 1] = 0x01; - - return buf; -} - void mptflags::operator()(Env& env) const { @@ -140,8 +125,7 @@ MPTTester::MPTTester(MPTInitDef const& arg) { } -MPTTester:: -operator MPT() const +MPTTester::operator MPT() const { if (!id_) Throw("MPT has not been created"); @@ -479,8 +463,13 @@ MPTTester::set(MPTSet const& arg) return forObject([&](SLEP const& sle) -> bool { if (sle) { + auto const issuerPubKey = getPubKey(issuer_); + if (!issuerPubKey) + Throw( + "MPTTester::set: issuer's pubkey is not set"); + return strHex((*sle)[sfIssuerElGamalPublicKey]) == - strHex(getPubKey(issuer_)); + strHex(*issuerPubKey); } return false; }); @@ -496,8 +485,14 @@ MPTTester::set(MPTSet const& arg) if (!auditor_) Throw( "MPTTester::set: auditor is not set"); + + auto const auditorPubKey = getPubKey(*auditor_); + if (!auditorPubKey) + Throw( + "MPTTester::set: auditor's pubkey is not set"); + return strHex((*sle)[sfAuditorElGamalPublicKey]) == - strHex(getPubKey(*auditor_)); + strHex(*auditorPubKey); } return false; }); @@ -695,8 +690,7 @@ 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"); @@ -733,7 +727,7 @@ MPTTester::getIssuanceConfidentialBalance() const return 0; } -Buffer +std::optional MPTTester::getClawbackProof( Account const& holder, std::uint64_t amount, @@ -746,45 +740,35 @@ MPTTester::getClawbackProof( auto const sleHolder = env_.le(keylet::mptoken(*id_, holder.id())); auto const sleIssuance = env_.le(keylet::mptIssuance(*id_)); - // helper to generate a dummy proof, so that other preclaim tests can - // proceed - auto const getDummyProof = []() { - Buffer dummy(ecEqualityProofLength); - std::memset(dummy.data(), 0, ecEqualityProofLength); - return dummy; - }; - - if (!sleHolder) - return getDummyProof(); - - if (!sleIssuance) - Throw("Issuance object not found"); + if (!sleHolder || !sleIssuance) + return std::nullopt; auto const ciphertextBlob = sleHolder->getFieldVL(sfIssuerEncryptedBalance); - if (ciphertextBlob.size() == 0) - return getDummyProof(); + if (ciphertextBlob.size() != ecGamalEncryptedTotalLength) + return std::nullopt; auto const pubKeyBlob = sleIssuance->getFieldVL(sfIssuerElGamalPublicKey); - Slice const ciphertext(ciphertextBlob.data(), ciphertextBlob.size()); - Slice const pubKey(pubKeyBlob.data(), pubKeyBlob.size()); + if (pubKeyBlob.size() != ecPubKeyLength) + return std::nullopt; - if (ciphertextBlob.size() != ecGamalEncryptedTotalLength) - Throw("Invalid Ciphertext length"); - - secp256k1_pubkey c1, c2; + secp256k1_pubkey c1, c2, pk; auto const ctx = secp256k1Context(); + + if (!secp256k1_ec_pubkey_parse( + ctx, &c1, ciphertextBlob.data(), ecGamalEncryptedLength)) + { + return std::nullopt; + } + if (!secp256k1_ec_pubkey_parse( - ctx, &c1, ciphertextBlob.data(), ecGamalEncryptedLength) || - !secp256k1_ec_pubkey_parse( ctx, &c2, ciphertextBlob.data() + ecGamalEncryptedLength, ecGamalEncryptedLength)) { - Throw("Invalid Ciphertext"); + return std::nullopt; } - secp256k1_pubkey pk; std::memcpy(pk.data, pubKeyBlob.data(), ecPubKeyLength); Buffer proof(ecEqualityProofLength); @@ -798,24 +782,25 @@ MPTTester::getClawbackProof( privateKey.data(), contextHash.data()) != 1) { - Throw("Proof generation failed"); + return std::nullopt; } return proof; } -Buffer -MPTTester::getSchnorrProof(Account const& account, uint256 const& contextHash) - const +std::optional +MPTTester::getSchnorrProof(Account const& account, uint256 const& ctxHash) const { auto const pubKey = getPubKey(account); - auto const privKey = getPrivKey(account); + if (!pubKey || pubKey->size() != ecPubKeyLength) + return std::nullopt; - if (pubKey.size() != ecPubKeyLength) - Throw("Invalid public key size"); + auto const privKey = getPrivKey(account); + if (privKey->size() != ecPrivKeyLength) + return std::nullopt; secp256k1_pubkey pk; - std::memcpy(pk.data, pubKey.data(), ecPubKeyLength); + std::memcpy(pk.data, pubKey->data(), ecPubKeyLength); Buffer proof(ecSchnorrProofLength); @@ -823,10 +808,89 @@ MPTTester::getSchnorrProof(Account const& account, uint256 const& contextHash) secp256k1Context(), proof.data(), &pk, - privKey.data(), + privKey->data(), + ctxHash.data()) != 1) + { + return std::nullopt; + } + + return proof; +} + +std::optional +MPTTester::getConfidentialSendProof( + std::uint64_t const amount, + std::vector const& recipients, + Slice const& blindingFactor, + std::size_t const nRecipients, + uint256 const& contextHash) const +{ + if (recipients.size() != nRecipients) + return std::nullopt; + + if (blindingFactor.size() != ecBlindingFactorLength) + return std::nullopt; + + auto const ctx = secp256k1Context(); + + std::vector r(nRecipients); + std::vector s(nRecipients); + std::vector pk(nRecipients); + + std::vector sr; + sr.reserve(nRecipients * ecBlindingFactorLength); + + for (size_t i = 0; i < nRecipients; ++i) + { + auto const& recipient = recipients[i]; + auto const* ctData = recipient.encryptedAmount.data(); + + if (recipient.encryptedAmount.size() != ecGamalEncryptedTotalLength) + return std::nullopt; + + if (recipient.publicKey.size() != ecPubKeyLength) + return std::nullopt; + + if (!secp256k1_ec_pubkey_parse( + ctx, &r[i], ctData, ecGamalEncryptedLength)) + { + return std::nullopt; + } + + if (!secp256k1_ec_pubkey_parse( + ctx, + &s[i], + ctData + ecGamalEncryptedLength, + ecGamalEncryptedLength)) + { + return std::nullopt; + } + + std::memcpy(pk[i].data, recipient.publicKey.data(), ecPubKeyLength); + + sr.insert( + sr.end(), + blindingFactor.data(), + blindingFactor.data() + ecBlindingFactorLength); + } + + size_t proofLen = + secp256k1_mpt_prove_same_plaintext_multi_size(nRecipients); + Buffer proof(proofLen); + + if (secp256k1_mpt_prove_same_plaintext_multi( + ctx, + proof.data(), + &proofLen, + amount, + nRecipients, + r.data(), + s.data(), + pk.data(), + sr.data(), contextHash.data()) != 1) { - Throw("Schnorr Proof generation failed"); + return std::nullopt; } return proof; @@ -874,11 +938,17 @@ MPTTester::getConvertBackProof( !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending)) return Buffer{}; - Buffer const pedersenProof = generatePedersenLinkageProof( - holder, contextHash, getPubKey(holder), pcParams); + auto const holderPubKey = getPubKey(holder); + if (holderPubKey) + { + Buffer const pedersenProof = generatePedersenLinkageProof( + holder, contextHash, *holderPubKey, pcParams); - // todo: incoporate range proof - return pedersenProof; + // todo: incoporate range proof + return pedersenProof; + } + + return Buffer{}; } std::optional @@ -1032,21 +1102,34 @@ MPTTester::convert(MPTConvert const& arg) auto const contextHash = getConvertContextHash( arg.account->id(), env_.seq(*arg.account), *id_, *arg.amt); - Buffer proof = getSchnorrProof(*arg.account, contextHash); - jv[sfZKProof.jsonName] = strHex(proof); + auto const proof = getSchnorrProof(*arg.account, contextHash); + if (proof) + jv[sfZKProof.jsonName] = strHex(*proof); + else + jv[sfZKProof.jsonName] = strHex(Buffer(ecSchnorrProofLength)); } auto const holderAmt = getBalance(*arg.account); auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance(); - uint64_t prevInboxBalance = + auto const prevInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); - uint64_t prevSpendingBalance = + auto const prevSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); - uint64_t prevIssuerBalance = + auto const prevIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); - [[maybe_unused]] uint64_t prevAuditorBalance = - getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); + + if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance) + Throw("Failed to get Pre-convert balance"); + + std::optional prevAuditorBalance; + if (arg.auditorEncryptedAmt || auditor_) + { + prevAuditorBalance = + getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); + if (!prevAuditorBalance) + Throw("Failed to get Pre-convert balance"); + } if (submit(arg, jv) == tesSUCCESS) { @@ -1058,41 +1141,49 @@ MPTTester::convert(MPTConvert const& arg) postConfidentialOutstanding; })); - uint64_t postInboxBalance = + auto const postInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); - uint64_t postIssuerBalance = + auto const postIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); - uint64_t postSpendingBalance = + auto const postSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance) + Throw("Failed to get post-convert balance"); + if (arg.auditorEncryptedAmt || auditor_) { - uint64_t postAuditorBalance = + auto const postAuditorBalance = getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); + + if (!postAuditorBalance) + Throw("Failed to get post-convert balance"); + // auditor's encrypted balance is updated correctly env_.require(requireAny([&]() -> bool { - return prevAuditorBalance + *arg.amt == postAuditorBalance; + return *prevAuditorBalance + *arg.amt == *postAuditorBalance; })); } // spending balance should not change env_.require(requireAny([&]() -> bool { - return postSpendingBalance == prevSpendingBalance; + return *postSpendingBalance == *prevSpendingBalance; })); // issuer's encrypted balance is updated correctly env_.require(requireAny([&]() -> bool { - return prevIssuerBalance + *arg.amt == postIssuerBalance; + return *prevIssuerBalance + *arg.amt == *postIssuerBalance; })); // holder's inbox balance is updated correctly env_.require(requireAny([&]() -> bool { - return prevInboxBalance + *arg.amt == postInboxBalance; + return *prevInboxBalance + *arg.amt == *postInboxBalance; })); // sum of holder's inbox and spending balance should equal to issuer's // encrypted balance env_.require(requireAny([&]() -> bool { - return postInboxBalance + postSpendingBalance == postIssuerBalance; + return *postInboxBalance + *postSpendingBalance == + *postIssuerBalance; })); if (arg.holderPubKey) @@ -1102,8 +1193,14 @@ MPTTester::convert(MPTConvert const& arg) [&](SLEP const& sle) -> bool { if (sle) { + auto const holderPubKey = getPubKey(*arg.account); + if (!holderPubKey) + Throw( + "MPTTester::convert: holder's pubkey is " + "not set"); + return strHex((*sle)[sfHolderElGamalPublicKey]) == - strHex(getPubKey(*arg.account)); + strHex(*holderPubKey); } return false; }, @@ -1117,6 +1214,8 @@ void MPTTester::send(MPTConfidentialSend const& arg) { Json::Value jv; + jv[jss::TransactionType] = jss::ConfidentialSend; + if (arg.account) jv[sfAccount] = arg.account->human(); else @@ -1127,7 +1226,9 @@ MPTTester::send(MPTConfidentialSend const& arg) else Throw("Destination not specified"); - jv[jss::TransactionType] = jss::ConfidentialSend; + if (!arg.amt) + Throw("Amount not specified for testing purposes"); + if (arg.id) jv[sfMPTokenIssuanceID] = to_string(*arg.id); else @@ -1140,33 +1241,83 @@ MPTTester::send(MPTConfidentialSend const& arg) Buffer const blindingFactor = arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor(); - // Generate the encrypted amounts if not provided - if (arg.senderEncryptedAmt) - jv[sfSenderEncryptedAmount] = strHex(*arg.senderEncryptedAmt); - else - jv[sfSenderEncryptedAmount] = - strHex(encryptAmount(*arg.account, *arg.amt, blindingFactor)); - - if (arg.destEncryptedAmt) - jv[sfDestinationEncryptedAmount] = strHex(*arg.destEncryptedAmt); - else - jv[sfDestinationEncryptedAmount] = - strHex(encryptAmount(*arg.dest, *arg.amt, blindingFactor)); - - if (arg.issuerEncryptedAmt) - jv[sfIssuerEncryptedAmount] = strHex(*arg.issuerEncryptedAmt); - else - jv[sfIssuerEncryptedAmount] = - strHex(encryptAmount(issuer_, *arg.amt, blindingFactor)); + // fill in the encrypted amounts if not provided + auto const senderAmt = arg.senderEncryptedAmt + ? *arg.senderEncryptedAmt + : encryptAmount(*arg.account, *arg.amt, blindingFactor); + auto const destAmt = arg.destEncryptedAmt + ? *arg.destEncryptedAmt + : encryptAmount(*arg.dest, *arg.amt, blindingFactor); + auto const issuerAmt = arg.issuerEncryptedAmt + ? *arg.issuerEncryptedAmt + : encryptAmount(issuer_, *arg.amt, blindingFactor); + std::optional auditorAmt; if (arg.auditorEncryptedAmt) - jv[sfAuditorEncryptedAmount] = strHex(*arg.auditorEncryptedAmt); - else if (auditor()) - jv[sfAuditorEncryptedAmount] = - strHex(encryptAmount(*auditor(), *arg.amt, blindingFactor)); + auditorAmt = arg.auditorEncryptedAmt; + else if (auditor_) + auditorAmt = encryptAmount(*auditor_, *arg.amt, blindingFactor); + jv[sfSenderEncryptedAmount] = strHex(senderAmt); + jv[sfDestinationEncryptedAmount] = strHex(destAmt); + jv[sfIssuerEncryptedAmount] = strHex(issuerAmt); + if (auditorAmt) + jv[sfAuditorEncryptedAmount] = strHex(*auditorAmt); + + // fill in the proof if not provided if (arg.proof) jv[sfZKProof] = *arg.proof; + else + { + auto const version = getMPTokenVersion(*arg.account); + auto const ctxHash = getSendContextHash( + arg.account->id(), + env_.seq(*arg.account), + *id_, + arg.dest->id(), + version); + + auto const nRecipients = auditorAmt ? 4 : 3; + std::vector recipients; + + auto const senderPubKey = getPubKey(*arg.account); + auto const destPubKey = getPubKey(*arg.dest); + auto const issuerPubKey = getPubKey(issuer_); + + // If a key is missing, we skip adding the recipient. This intentionally + // causes proof generation to fail (due to recipient count mismatch), + // triggering the dummy proof fallback. + if (senderPubKey) + recipients.push_back({Slice(*senderPubKey), senderAmt}); + if (destPubKey) + recipients.push_back({Slice(*destPubKey), destAmt}); + if (issuerPubKey) + recipients.push_back({Slice(*issuerPubKey), issuerAmt}); + + std::optional auditorPubKey; + if (auditorAmt) + { + if (!auditor_) + Throw("Auditor not registered"); + + auditorPubKey = getPubKey(*auditor_); + if (auditorPubKey) + recipients.push_back({Slice(*auditorPubKey), *auditorAmt}); + } + + auto const proof = getConfidentialSendProof( + *arg.amt, recipients, blindingFactor, nRecipients, ctxHash); + + if (proof) + jv[sfZKProof.jsonName] = strHex(*proof); + else + { + size_t const dummySize = + secp256k1_mpt_prove_same_plaintext_multi_size(nRecipients); + + jv[sfZKProof.jsonName] = strHex(Buffer(dummySize)); + } + } if (arg.credentials) { @@ -1181,24 +1332,41 @@ MPTTester::send(MPTConfidentialSend const& arg) auto const prevOA = getIssuanceOutstandingBalance(); // Sender's previous confidential state - uint64_t prevSenderInbox = + auto const prevSenderInbox = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); - uint64_t prevSenderSpending = + auto const prevSenderSpending = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); - uint64_t prevSenderIssuer = + auto const prevSenderIssuer = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); - [[maybe_unused]] uint64_t prevSenderAuditor = - getDecryptedBalance(*arg.account, AUDITOR_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 - uint64_t prevDestInbox = + auto const prevDestInbox = getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX); - uint64_t prevDestSpending = + auto const prevDestSpending = getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING); - uint64_t prevDestIssuer = + auto const prevDestIssuer = getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE); - [[maybe_unused]] uint64_t prevDestAuditor = - getDecryptedBalance(*arg.dest, AUDITOR_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) { @@ -1206,21 +1374,27 @@ MPTTester::send(MPTConfidentialSend const& arg) auto const postOA = getIssuanceOutstandingBalance(); // Sender's post confidential state - uint64_t postSenderInbox = + auto const postSenderInbox = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); - uint64_t postSenderSpending = + auto const postSenderSpending = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); - uint64_t postSenderIssuer = + auto const postSenderIssuer = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); + if (!postSenderInbox || !postSenderSpending || !postSenderIssuer) + Throw("Failed to get Post-send balance"); + // Destination's post confidential state - uint64_t postDestInbox = + auto const postDestInbox = getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX); - uint64_t postDestSpending = + auto const postDestSpending = getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING); - uint64_t postDestIssuer = + auto const postDestIssuer = getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE); + if (!postDestInbox || !postDestSpending || !postDestIssuer) + Throw("Failed to get Post-send balance"); + // Public balances unchanged env_.require(mptbalance(*this, *arg.account, senderPubAmt)); env_.require(mptbalance(*this, *arg.dest, destPubAmt)); @@ -1231,55 +1405,57 @@ MPTTester::send(MPTConfidentialSend const& arg) // Verify sender changes env_.require(requireAny([&]() -> bool { - return prevSenderSpending >= *arg.amt && - postSenderSpending == prevSenderSpending - *arg.amt; + return *prevSenderSpending >= *arg.amt && + *postSenderSpending == *prevSenderSpending - *arg.amt; })); env_.require(requireAny( [&]() -> bool { return postSenderInbox == prevSenderInbox; })); env_.require(requireAny([&]() -> bool { - return prevSenderIssuer >= *arg.amt && - postSenderIssuer == prevSenderIssuer - *arg.amt; + return *prevSenderIssuer >= *arg.amt && + *postSenderIssuer == *prevSenderIssuer - *arg.amt; })); // Verify destination changes env_.require(requireAny([&]() -> bool { - return postDestInbox == prevDestInbox + *arg.amt; + return *postDestInbox == *prevDestInbox + *arg.amt; })); env_.require(requireAny( - [&]() -> bool { return postDestSpending == prevDestSpending; })); + [&]() -> bool { return *postDestSpending == *prevDestSpending; })); env_.require(requireAny([&]() -> bool { - return postDestIssuer == prevDestIssuer + *arg.amt; + return *postDestIssuer == *prevDestIssuer + *arg.amt; })); // Cross checks env_.require(requireAny([&]() -> bool { - return postSenderInbox + postSenderSpending == postSenderIssuer; + return *postSenderInbox + *postSenderSpending == *postSenderIssuer; })); env_.require(requireAny([&]() -> bool { - return postDestInbox + postDestSpending == postDestIssuer; + return *postDestInbox + *postDestSpending == *postDestIssuer; })); if (arg.auditorEncryptedAmt || auditor_) { - uint64_t postSenderAuditor = + auto const postSenderAuditor = getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); - uint64_t postDestAuditor = + auto const postDestAuditor = getDecryptedBalance(*arg.dest, AUDITOR_ENCRYPTED_BALANCE); + if (!postSenderAuditor || !postDestAuditor) + Throw("Failed to get Post-send balance"); env_.require(requireAny([&]() -> bool { - return postSenderAuditor == postSenderIssuer && - postDestAuditor == postDestIssuer; + return *postSenderAuditor == *postSenderIssuer && + *postDestAuditor == *postDestIssuer; })); // verify sender env_.require(requireAny([&]() -> bool { return prevSenderAuditor >= *arg.amt && - postSenderAuditor == prevSenderAuditor - *arg.amt; + *postSenderAuditor == *prevSenderAuditor - *arg.amt; })); // verify dest env_.require(requireAny([&]() -> bool { - return postDestAuditor == prevDestAuditor + *arg.amt; + return *postDestAuditor == *prevDestAuditor + *arg.amt; })); } } @@ -1315,10 +1491,18 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg) std::uint32_t const seq = env_.seq(account); uint256 const contextHash = getClawbackContextHash( account.id(), seq, *id_, *arg.amt, arg.holder->id()); - Buffer proof = getClawbackProof( - *arg.holder, *arg.amt, getPrivKey(account), contextHash); - jv[sfZKProof] = strHex(proof); + auto const privKey = getPrivKey(account); + if (!privKey || privKey->size() != ecPrivKeyLength) + Throw("Failed to get clawback private key"); + + auto const proof = + getClawbackProof(*arg.holder, *arg.amt, *privKey, contextHash); + + if (proof) + jv[sfZKProof] = strHex(*proof); + else + jv[sfZKProof] = strHex(Buffer(ecEqualityProofLength)); } auto const holderPubAmt = getBalance(*arg.holder); @@ -1374,7 +1558,7 @@ MPTTester::generateKeyPair(Account const& account) privKeys.insert({account.id(), Buffer{privKey, ecPrivKeyLength}}); } -Buffer +std::optional MPTTester::getPubKey(Account const& account) const { auto it = pubKeys.find(account.id()); @@ -1383,10 +1567,10 @@ MPTTester::getPubKey(Account const& account) const return it->second; } - Throw("Account does not have public key"); + return std::nullopt; } -Buffer +std::optional MPTTester::getPrivKey(Account const& account) const { auto it = privKeys.find(account.id()); @@ -1395,7 +1579,7 @@ MPTTester::getPrivKey(Account const& account) const return it->second; } - Throw("Account does not have private key"); + return std::nullopt; } Buffer @@ -1404,60 +1588,69 @@ MPTTester::encryptAmount( uint64_t const amt, Buffer const& blindingFactor) const { - auto const result = - ripple::encryptAmount(amt, getPubKey(account), blindingFactor); - - if (result) - return *result; + if (auto const pubKey = getPubKey(account)) + { + if (auto const result = + ripple::encryptAmount(amt, *pubKey, blindingFactor)) + return *result; + } // Return a dummy buffer on failure to allow testing of // failures that occur prior to encryption. return Buffer(ecGamalEncryptedTotalLength); } -uint64_t +std::optional MPTTester::decryptAmount(Account const& account, Buffer const& amt) const { + if (amt.size() != ecGamalEncryptedTotalLength) + return std::nullopt; + secp256k1_pubkey c1; secp256k1_pubkey c2; - uint64_t decryptedAmt; - if (!makeEcPair(amt, c1, c2)) - Throw( - "Failed to convert into individual EC components"); + return std::nullopt; + auto const privKey = getPrivKey(account); + if (!privKey || privKey->size() != ecPrivKeyLength) + return std::nullopt; + + uint64_t decryptedAmt; if (!secp256k1_elgamal_decrypt( - secp256k1Context(), - &decryptedAmt, - &c1, - &c2, - getPrivKey(account).data())) - Throw("Failed to decrypt amount"); + secp256k1Context(), &decryptedAmt, &c1, &c2, privKey->data())) + { + return std::nullopt; + } return decryptedAmt; } -uint64_t +std::optional MPTTester::getDecryptedBalance( Account const& account, EncryptedBalanceType balanceType) const { - auto maybeEncrypted = getEncryptedBalance(account, balanceType); - Account accountToDecrypt = account; + auto encryptedAmt = getEncryptedBalance(account, balanceType); + + // Return zero to test cases like Feature Disabled, where the ledger object + // does not exist. + if (!encryptedAmt) + return 0; + + Account decryptor = account; if (balanceType == ISSUER_ENCRYPTED_BALANCE) - accountToDecrypt = issuer_; + decryptor = issuer_; else if (balanceType == AUDITOR_ENCRYPTED_BALANCE) { if (!auditor_) - return 0; - accountToDecrypt = *auditor_; + return std::nullopt; + decryptor = *auditor_; } - return maybeEncrypted ? decryptAmount(accountToDecrypt, *maybeEncrypted) - : 0; + return decryptAmount(decryptor, *encryptedAmt); }; void @@ -1476,33 +1669,43 @@ MPTTester::mergeInbox(MPTMergeInbox const& arg) Throw("MPT has not been created"); jv[sfMPTokenIssuanceID] = to_string(*id_); } + jv[sfTransactionType] = jss::ConfidentialMergeInbox; - uint64_t prevInboxBalance = + auto const prevInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); - uint64_t prevSpendingBalance = + auto const prevSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); - uint64_t prevIssuerBalance = + auto const prevIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); + + if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance) + Throw("Failed to get pre-mergeInbox balances"); + if (submit(arg, jv) == tesSUCCESS) { - uint64_t postInboxBalance = + auto const postInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); - uint64_t postSpendingBalance = + auto const postSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); - uint64_t postIssuerBalance = + auto const postIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); + if (!postInboxBalance || !postSpendingBalance || !postIssuerBalance) + Throw("Failed to get post-mergeInbox balances"); + env_.require(requireAny([&]() -> bool { - return postSpendingBalance == - prevInboxBalance + prevSpendingBalance && - postInboxBalance == 0; + return *postSpendingBalance == + *prevInboxBalance + *prevSpendingBalance && + *postInboxBalance == 0; })); - env_.require(requireAny( - [&]() -> bool { return prevIssuerBalance == postIssuerBalance; })); + env_.require(requireAny([&]() -> bool { + return *prevIssuerBalance == *postIssuerBalance; + })); env_.require(requireAny([&]() -> bool { - return postSpendingBalance + postInboxBalance == postIssuerBalance; + return *postSpendingBalance + *postInboxBalance == + *postIssuerBalance; })); } } @@ -1575,8 +1778,15 @@ MPTTester::convertBack(MPTConvertBack const& arg) jv[sfBlindingFactor] = strHex(blindingFactor); - uint64_t prevSpendingBalance = + auto const prevInboxBalance = + getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); + auto const prevSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + auto const prevIssuerBalance = + getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); + + if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance) + Throw("Failed to get Pre-convertBack balance"); Buffer pedersenCommitment; Buffer pcBlindingFactor = generateBlindingFactor(); @@ -1584,7 +1794,7 @@ MPTTester::convertBack(MPTConvertBack const& arg) pedersenCommitment = *arg.pedersenCommitment; else pedersenCommitment = - getPedersenCommitment(prevSpendingBalance, pcBlindingFactor); + getPedersenCommitment(*prevSpendingBalance, pcBlindingFactor); jv[sfPedersenCommitment] = strHex(pedersenCommitment); @@ -1618,7 +1828,7 @@ MPTTester::convertBack(MPTConvertBack const& arg) blindingFactor, { .pedersenCommitment = pedersenCommitment, - .amt = prevSpendingBalance, + .amt = *prevSpendingBalance, .encryptedAmt = *prevEncryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); @@ -1629,12 +1839,14 @@ MPTTester::convertBack(MPTConvertBack const& arg) auto const holderAmt = getBalance(*arg.account); auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance(); - uint64_t prevInboxBalance = - getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); - uint64_t prevIssuerBalance = - getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); - [[maybe_unused]] uint64_t prevAuditorBalance = - getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); + std::optional prevAuditorBalance; + if (arg.auditorEncryptedAmt || auditor_) + { + prevAuditorBalance = + getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); + if (!prevAuditorBalance) + Throw("Failed to get Pre-convertBack balance"); + } if (submit(arg, jv) == tesSUCCESS) { @@ -1646,41 +1858,50 @@ MPTTester::convertBack(MPTConvertBack const& arg) postConfidentialOutstanding; })); - uint64_t postInboxBalance = + auto const postInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); - uint64_t postIssuerBalance = + auto const postIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); - uint64_t postSpendingBalance = + auto const postSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance) + Throw("Failed to get post-convertBack balance"); + if (arg.auditorEncryptedAmt || auditor_) { - uint64_t postAuditorBalance = + auto const postAuditorBalance = getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); + + if (!postAuditorBalance) + Throw( + "Failed to get post-convertBack balance"); + // auditor's encrypted balance is updated correctly env_.require(requireAny([&]() -> bool { - return prevAuditorBalance - *arg.amt == postAuditorBalance; + return *prevAuditorBalance - *arg.amt == *postAuditorBalance; })); } // inbox balance should not change env_.require(requireAny( - [&]() -> bool { return postInboxBalance == prevInboxBalance; })); + [&]() -> bool { return *postInboxBalance == *prevInboxBalance; })); // issuer's encrypted balance is updated correctly env_.require(requireAny([&]() -> bool { - return prevIssuerBalance - *arg.amt == postIssuerBalance; + return *prevIssuerBalance - *arg.amt == *postIssuerBalance; })); // holder's spending balance is updated correctly env_.require(requireAny([&]() -> bool { - return prevSpendingBalance - *arg.amt == postSpendingBalance; + return *prevSpendingBalance - *arg.amt == *postSpendingBalance; })); // sum of holder's inbox and spending balance should equal to issuer's // encrypted balance env_.require(requireAny([&]() -> bool { - return postInboxBalance + postSpendingBalance == postIssuerBalance; + return *postInboxBalance + *postSpendingBalance == + *postIssuerBalance; })); } } @@ -1720,6 +1941,10 @@ MPTTester::generatePedersenLinkageProof( Buffer proof(ecPedersenProofLength); + auto const privKey = getPrivKey(account); + if (!privKey || privKey->size() != ecPrivKeyLength) + Throw("Failed to get Pedersen proof private key"); + if (secp256k1_elgamal_pedersen_link_prove( ctx, proof.data(), @@ -1728,7 +1953,7 @@ MPTTester::generatePedersenLinkageProof( &c1, &pcm, params.amt, - getPrivKey(account).data(), + privKey->data(), params.blindingFactor.data(), contextHash.data()) != 1) Throw("Pedersen proof generation failed"); diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index a3cbc466b1..0b2d34221d 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -19,10 +19,6 @@ class MPTTester; auto const MPTDEXFlags = tfMPTCanTrade | tfMPTCanTransfer; -// Generates a syntactically valid placeholder ciphertext -ripple::Buffer -generatePlaceholderCiphertext(); - // Check flags settings on MPT create class mptflags { @@ -434,10 +430,10 @@ public: void generateKeyPair(Account const& account); - Buffer + std::optional getPubKey(Account const& account) const; - Buffer + std::optional getPrivKey(Account const& account) const; Buffer @@ -446,10 +442,10 @@ public: uint64_t const amt, Buffer const& blindingFactor) const; - uint64_t + std::optional decryptAmount(Account const& account, Buffer const& amt) const; - uint64_t + std::optional getDecryptedBalance( Account const& account, EncryptedBalanceType balanceType) const; @@ -457,15 +453,23 @@ public: std::int64_t getIssuanceOutstandingBalance() const; - Buffer + std::optional getClawbackProof( Account const& holder, std::uint64_t amount, Buffer const& privateKey, uint256 const& txHash) const; - Buffer - getSchnorrProof(Account const& account, uint256 const& contextHash) const; + std::optional + getSchnorrProof(Account const& account, uint256 const& ctxHash) const; + + std::optional + getConfidentialSendProof( + std::uint64_t const amount, + std::vector const& recipients, + Slice const& blindingFactor, + std::size_t const nRecipients, + uint256 const& contextHash) const; Buffer getConvertBackProof( diff --git a/src/xrpld/app/tx/detail/ConfidentialConvert.cpp b/src/xrpld/app/tx/detail/ConfidentialConvert.cpp index f72898084d..feacc3036d 100644 --- a/src/xrpld/app/tx/detail/ConfidentialConvert.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialConvert.cpp @@ -139,11 +139,10 @@ ConfidentialConvert::preclaim(PreclaimContext const& ctx) holderPubKey = (*sleMptoken)[sfHolderElGamalPublicKey]; } - // Prepare Auditor Info - std::optional auditor; + std::optional auditor; if (hasAuditor) { - auditor.emplace(EncryptedAmountInfo{ + auditor.emplace(ConfidentialRecipient{ (*sleIssuance)[sfAuditorElGamalPublicKey], ctx.tx[sfAuditorEncryptedAmount]}); } diff --git a/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp b/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp index 493b7aa225..04e1cf4a8a 100644 --- a/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp @@ -62,14 +62,13 @@ verifyProofs( (*mptoken)[~sfConfidentialBalanceVersion].value_or(0)); // Prepare Auditor Info - std::optional auditor; + std::optional auditor; bool const hasAuditor = issuance->isFieldPresent(sfAuditorElGamalPublicKey); if (hasAuditor) { - auditor.emplace( - EncryptedAmountInfo{ - (*issuance)[sfAuditorElGamalPublicKey], - tx[sfAuditorEncryptedAmount]}); + auditor.emplace(ConfidentialRecipient{ + (*issuance)[sfAuditorElGamalPublicKey], + tx[sfAuditorEncryptedAmount]}); } if (auto const ter = verifyRevealedAmount( @@ -200,10 +199,6 @@ ConfidentialConvertBack::doApply() (*sleIssuance)[sfConfidentialOutstandingAmount] = (*sleIssuance)[sfConfidentialOutstandingAmount] - amtToConvertBack; - // it's fine if it reaches max uint32, it just resets to 0 - (*sleMptoken)[sfConfidentialBalanceVersion] = - (*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0u) + 1u; - std::optional const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount]; // homomorphically subtract holder's encrypted balance @@ -245,6 +240,9 @@ ConfidentialConvertBack::doApply() (*sleMptoken)[sfAuditorEncryptedBalance] = res; } + // increment version + incrementConfidentialVersion(*sleMptoken); + view().update(sleIssuance); view().update(sleMptoken); return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/ConfidentialSend.cpp b/src/xrpld/app/tx/detail/ConfidentialSend.cpp index 4b63a4f4bf..2996d13606 100644 --- a/src/xrpld/app/tx/detail/ConfidentialSend.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialSend.cpp @@ -43,11 +43,11 @@ ConfidentialSend::preflight(PreflightContext const& ctx) ecGamalEncryptedTotalLength) return temBAD_CIPHERTEXT; - if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount])) - return temBAD_CIPHERTEXT; - - // if (ctx.tx[sfZKProof].length() != ecEqualityProofLength) - // return temMALFORMED; + // Check the length of the ZKProof + auto const recipientCount = getConfidentialRecipientCount(hasAuditor); + if (ctx.tx[sfZKProof].length() != + getMultiCiphertextEqualityProofSize(recipientCount)) + return temMALFORMED; // Check the encrypted amount formats, this is more expensive so put it at // the end @@ -56,6 +56,9 @@ ConfidentialSend::preflight(PreflightContext const& ctx) !isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount])) return temBAD_CIPHERTEXT; + if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount])) + return temBAD_CIPHERTEXT; + return tesSUCCESS; } @@ -152,6 +155,50 @@ 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 = requiresAuditor ? 4 : 3; + + // 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; } @@ -226,10 +273,6 @@ ConfidentialSend::doApply() (*sleSender)[sfAuditorEncryptedBalance] = newAuditorEnc; } - // Increment version - (*sleSender)[sfConfidentialBalanceVersion] = - (*sleSender)[~sfConfidentialBalanceVersion].value_or(0u) + 1u; - // Add to destination's inbox balance { Slice const curInbox = (*sleDestination)[sfConfidentialBalanceInbox]; @@ -270,6 +313,10 @@ ConfidentialSend::doApply() (*sleDestination)[sfAuditorEncryptedBalance] = newAuditorEnc; } + // increment version + incrementConfidentialVersion(*sleSender); + incrementConfidentialVersion(*sleDestination); + view().update(sleSender); view().update(sleDestination); return tesSUCCESS;