diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index be349dd96e..cec3370ba7 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -10,12 +10,30 @@ #include #include #include +#include #include #include namespace ripple { +void +addCommonZKPFields( + Serializer& s, + std::uint16_t txType, + AccountID const& account, + std::uint32_t sequence, + uint192 const& issuanceID, + std::uint64_t amount); + +uint256 +getClawbackContextHash( + AccountID const& account, + std::uint32_t sequence, + uint192 const& issuanceID, + std::uint64_t amount, + AccountID const& holder); + /** * @brief Generates a new secp256k1 key pair. */ @@ -156,6 +174,82 @@ verifyConfidentialSendProof( std::uint32_t const version, uint256 const& txHash); +/** + * Generates a cryptographically secure 32-byte scalar (private key). + * @return 1 on success, 0 on failure. + */ +SECP256K1_API int +generate_random_scalar( + secp256k1_context const* ctx, + unsigned char* scalar_bytes); + +/** + * Computes the point M = amount * G. + * IMPORTANT: This function MUST NOT be called with amount = 0. + */ +SECP256K1_API int +compute_amount_point( + secp256k1_context const* ctx, + secp256k1_pubkey* mG, + uint64_t amount); + +/** + * Builds the challenge hash input for the NON-ZERO amount case. + * Output buffer must be 253 bytes. + */ +SECP256K1_API void +build_challenge_hash_input_nonzero( + unsigned char* hash_input, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk, + secp256k1_pubkey const* mG, + secp256k1_pubkey const* T1, + secp256k1_pubkey const* T2, + unsigned char const* tx_context_id); + +/** + * Builds the challenge hash input for the ZERO amount case. + * Output buffer must be 220 bytes. + */ +SECP256K1_API void +build_challenge_hash_input_zero( + unsigned char* hash_input, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk, + secp256k1_pubkey const* T1, + secp256k1_pubkey const* T2, + unsigned char const* tx_context_id); + +/** + * @brief Proves that a commitment (C1, C2) encrypts a specific plaintext + * 'amount'. + */ +SECP256K1_API int +secp256k1_equality_plaintext_prove( + secp256k1_context const* ctx, + unsigned char* proof, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk_recipient, + uint64_t amount, + unsigned char const* randomness_r, + unsigned char const* tx_context_id); + +/** + * @brief Verifies the proof generated by secp256k1_equality_plaintext_prove. + */ +SECP256K1_API int +secp256k1_equality_plaintext_verify( + secp256k1_context const* ctx, + unsigned char const* proof, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk_recipient, + uint64_t amount, + unsigned char const* tx_context_id); + } // namespace ripple #endif diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index a5a3507a6f..3346dbc097 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -6,6 +6,39 @@ #include namespace ripple { +void +addCommonZKPFields( + Serializer& s, + std::uint16_t txType, + AccountID const& account, + std::uint32_t sequence, + uint192 const& issuanceID, + std::uint64_t amount) +{ + s.add16(txType); + s.addBitString(account); + s.add32(sequence); + s.addBitString(issuanceID); + s.add64(amount); +} + +uint256 +getClawbackContextHash( + AccountID const& account, + std::uint32_t sequence, + uint192 const& issuanceID, + std::uint64_t amount, + AccountID const& holder) +{ + Serializer s; + addCommonZKPFields( + s, ttCONFIDENTIAL_CLAWBACK, account, sequence, issuanceID, amount); + + s.addBitString(holder); + + return s.getSHA512Half(); +} + int secp256k1_elgamal_generate_keypair( secp256k1_context const* ctx, @@ -585,4 +618,352 @@ verifyConfidentialSendProof( return tesSUCCESS; } +int +generate_random_scalar( + secp256k1_context const* ctx, + unsigned char* scalar_bytes) +{ + do + { + if (RAND_bytes(scalar_bytes, 32) != 1) + { + return 0; // Randomness failure + } + } while (secp256k1_ec_seckey_verify(ctx, scalar_bytes) != 1); + return 1; +} + +int +compute_amount_point( + secp256k1_context const* ctx, + secp256k1_pubkey* mG, + uint64_t amount) +{ + unsigned char amount_scalar[32] = {0}; + /* This function assumes amount != 0 */ + assert(amount != 0); + + /* Convert amount to 32-byte BIG-ENDIAN scalar */ + for (int i = 0; i < 8; ++i) + { + amount_scalar[31 - i] = (amount >> (i * 8)) & 0xFF; + } + return secp256k1_ec_pubkey_create(ctx, mG, amount_scalar); +} + +void +build_challenge_hash_input_nonzero( + unsigned char hash_input[253], + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk, + secp256k1_pubkey const* mG, + secp256k1_pubkey const* T1, + secp256k1_pubkey const* T2, + unsigned char const* tx_context_id) +{ + char const* domain_sep = "MPT_POK_PLAINTEXT_PROOF"; // 23 bytes + size_t offset = 0; + size_t len; + secp256k1_context* ser_ctx = + secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + memcpy(hash_input + offset, domain_sep, strlen(domain_sep)); + offset += strlen(domain_sep); + + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, c1, SECP256K1_EC_COMPRESSED); + offset += len; + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, c2, SECP256K1_EC_COMPRESSED); + offset += len; + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, pk, SECP256K1_EC_COMPRESSED); + offset += len; + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, mG, SECP256K1_EC_COMPRESSED); + offset += len; + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, T1, SECP256K1_EC_COMPRESSED); + offset += len; + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, T2, SECP256K1_EC_COMPRESSED); + offset += len; + + memcpy(hash_input + offset, tx_context_id, 32); + offset += 32; + + assert(offset == 253); + secp256k1_context_destroy(ser_ctx); +} + +void +build_challenge_hash_input_zero( + unsigned char hash_input[220], + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk, + secp256k1_pubkey const* T1, + secp256k1_pubkey const* T2, + unsigned char const* tx_context_id) +{ + char const* domain_sep = "MPT_POK_PLAINTEXT_PROOF"; // 23 bytes + size_t offset = 0; + size_t len; + secp256k1_context* ser_ctx = + secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + memcpy(hash_input + offset, domain_sep, strlen(domain_sep)); + offset += strlen(domain_sep); + + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, c1, SECP256K1_EC_COMPRESSED); + offset += len; + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, c2, SECP256K1_EC_COMPRESSED); + offset += len; + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, pk, SECP256K1_EC_COMPRESSED); + offset += len; + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, T1, SECP256K1_EC_COMPRESSED); + offset += len; + len = 33; + secp256k1_ec_pubkey_serialize( + ser_ctx, hash_input + offset, &len, T2, SECP256K1_EC_COMPRESSED); + offset += len; + + memcpy(hash_input + offset, tx_context_id, 32); + offset += 32; + + assert(offset == 220); + secp256k1_context_destroy(ser_ctx); +} + +int +secp256k1_equality_plaintext_prove( + secp256k1_context const* ctx, + unsigned char* proof, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk_recipient, + uint64_t amount, + unsigned char const* randomness_r, + unsigned char const* tx_context_id) +{ + /* C90 Declarations */ + unsigned char t_scalar[32]; + unsigned char e_scalar[32]; + unsigned char s_scalar[32]; + unsigned char er_scalar[32]; + secp256k1_pubkey T1, T2; + size_t len; + + /* Executable Code */ + + /* 1. Generate random scalar t */ + if (!generate_random_scalar(ctx, t_scalar)) + return 0; + + /* 2. Compute commitments T1 = t*G, T2 = t*Pk */ + if (!secp256k1_ec_pubkey_create(ctx, &T1, t_scalar)) + { + memset(t_scalar, 0, 32); + return 0; + } + T2 = *pk_recipient; + if (!secp256k1_ec_pubkey_tweak_mul(ctx, &T2, t_scalar)) + { + memset(t_scalar, 0, 32); + return 0; + } + + /* 3. Compute challenge e = H(...) */ + if (amount == 0) + { + unsigned char hash_input[220]; + build_challenge_hash_input_zero( + hash_input, c1, c2, pk_recipient, &T1, &T2, tx_context_id); + SHA256(hash_input, sizeof(hash_input), e_scalar); + } + else + { + secp256k1_pubkey mG; + unsigned char hash_input[253]; + if (!compute_amount_point(ctx, &mG, amount)) + { + memset(t_scalar, 0, 32); + return 0; + } + build_challenge_hash_input_nonzero( + hash_input, c1, c2, pk_recipient, &mG, &T1, &T2, tx_context_id); + SHA256(hash_input, sizeof(hash_input), e_scalar); + } + + /* Ensure e is a valid scalar */ + if (!secp256k1_ec_seckey_verify(ctx, e_scalar)) + { + memset(t_scalar, 0, 32); + return 0; + } + + /* 4. Compute s = (t + e*r) mod q */ + memcpy(er_scalar, randomness_r, 32); + if (!secp256k1_ec_seckey_tweak_mul(ctx, er_scalar, e_scalar)) + { + memset(t_scalar, 0, 32); + return 0; + } + memcpy(s_scalar, t_scalar, 32); + if (!secp256k1_ec_seckey_tweak_add(ctx, s_scalar, er_scalar)) + { + memset(t_scalar, 0, 32); + return 0; + } + + /* 5. Format the proof = T1(33) || T2(33) || s(32) */ + len = 33; + secp256k1_ec_pubkey_serialize( + ctx, proof, &len, &T1, SECP256K1_EC_COMPRESSED); + len = 33; + secp256k1_ec_pubkey_serialize( + ctx, proof + 33, &len, &T2, SECP256K1_EC_COMPRESSED); + memcpy(proof + 66, s_scalar, 32); + + /* 6. Clear secret data */ + memset(t_scalar, 0, 32); + memset(s_scalar, 0, 32); + memset(er_scalar, 0, 32); + + return 1; +} + +int +secp256k1_equality_plaintext_verify( + secp256k1_context const* ctx, + unsigned char const* proof, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + secp256k1_pubkey const* pk_recipient, + uint64_t amount, + unsigned char const* tx_context_id) +{ + /* C90 Declarations */ + secp256k1_pubkey T1, T2; + unsigned char s_scalar[32]; + unsigned char e_scalar[32]; + secp256k1_pubkey lhs_eq1, rhs_eq1_term2, rhs_eq1; + secp256k1_pubkey lhs_eq2, rhs_eq2, rhs_eq2_term2_base; + secp256k1_pubkey const* points_to_add[2]; + unsigned char lhs_bytes[33], rhs_bytes[33]; + size_t len; + + /* Executable Code */ + + /* 1. Deserialize proof into T1 (33), T2 (33), s_scalar (32) */ + if (secp256k1_ec_pubkey_parse(ctx, &T1, proof, 33) != 1) + return 0; + if (secp256k1_ec_pubkey_parse(ctx, &T2, proof + 33, 33) != 1) + return 0; + memcpy(s_scalar, proof + 66, 32); + if (!secp256k1_ec_seckey_verify(ctx, s_scalar)) + return 0; /* s cannot be 0 */ + + /* 2. Recompute challenge e' = H(...) */ + if (amount == 0) + { + unsigned char hash_input[220]; + build_challenge_hash_input_zero( + hash_input, c1, c2, pk_recipient, &T1, &T2, tx_context_id); + SHA256(hash_input, sizeof(hash_input), e_scalar); + } + else + { + secp256k1_pubkey mG; + unsigned char hash_input[253]; + if (!compute_amount_point(ctx, &mG, amount)) + return 0; + build_challenge_hash_input_nonzero( + hash_input, c1, c2, pk_recipient, &mG, &T1, &T2, tx_context_id); + SHA256(hash_input, sizeof(hash_input), e_scalar); + } + if (!secp256k1_ec_seckey_verify(ctx, e_scalar)) + return 0; /* e cannot be 0 */ + + /* 3. Check Equation 1: s*G == T1 + e'*C1 */ + if (!secp256k1_ec_pubkey_create(ctx, &lhs_eq1, s_scalar)) + return 0; + rhs_eq1_term2 = *c1; + if (!secp256k1_ec_pubkey_tweak_mul(ctx, &rhs_eq1_term2, e_scalar)) + return 0; + points_to_add[0] = &T1; + points_to_add[1] = &rhs_eq1_term2; + if (!secp256k1_ec_pubkey_combine(ctx, &rhs_eq1, points_to_add, 2)) + return 0; + + len = 33; + secp256k1_ec_pubkey_serialize( + ctx, lhs_bytes, &len, &lhs_eq1, SECP256K1_EC_COMPRESSED); + len = 33; + secp256k1_ec_pubkey_serialize( + ctx, rhs_bytes, &len, &rhs_eq1, SECP256K1_EC_COMPRESSED); + if (memcmp(lhs_bytes, rhs_bytes, 33) != 0) + return 0; // Eq 1 failed + + /* 4. Check Equation 2: s*Pk == T2 + e'*Y */ + /* 4a. LHS = s*Pk */ + lhs_eq2 = *pk_recipient; + if (!secp256k1_ec_pubkey_tweak_mul(ctx, &lhs_eq2, s_scalar)) + return 0; + + /* 4b. Define Y (the base for the second part of the proof) */ + if (amount == 0) + { + rhs_eq2_term2_base = *c2; // Y = C2 + } + else + { + secp256k1_pubkey mG; + compute_amount_point(ctx, &mG, amount); + if (!secp256k1_ec_pubkey_negate(ctx, &mG)) + return 0; + points_to_add[0] = c2; + points_to_add[1] = &mG; + if (!secp256k1_ec_pubkey_combine( + ctx, &rhs_eq2_term2_base, points_to_add, 2)) + return 0; // Y = C2 - mG + } + + /* 4c. RHS term = e'*Y */ + if (!secp256k1_ec_pubkey_tweak_mul(ctx, &rhs_eq2_term2_base, e_scalar)) + return 0; + /* 4d. RHS = T2 + (e'*Y) */ + points_to_add[0] = &T2; + points_to_add[1] = &rhs_eq2_term2_base; + if (!secp256k1_ec_pubkey_combine(ctx, &rhs_eq2, points_to_add, 2)) + return 0; + + /* 4e. Compare LHS == RHS */ + len = 33; + secp256k1_ec_pubkey_serialize( + ctx, lhs_bytes, &len, &lhs_eq2, SECP256K1_EC_COMPRESSED); + len = 33; + secp256k1_ec_pubkey_serialize( + ctx, rhs_bytes, &len, &rhs_eq2, SECP256K1_EC_COMPRESSED); + if (memcmp(lhs_bytes, rhs_bytes, 33) != 0) + return 0; // Eq 2 failed + + return 1; /* Both equations passed */ +} + } // namespace ripple diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index f62d6de042..e0ae9943f6 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -2066,17 +2066,17 @@ class ConfidentialTransfer_test : public beast::unit_test::suite // become zero after clawback, which is verified in the confidentialClaw // function. mptAlice.confidentialClaw( - {.account = alice, .holder = bob, .amt = 110, .proof = "123"}); + {.account = alice, .holder = bob, .amt = 110}); // alice clawback all confidential balance from carol, which is 70. // carol only has balance in spending. mptAlice.confidentialClaw( - {.account = alice, .holder = carol, .amt = 70, .proof = "123"}); + {.account = alice, .holder = carol, .amt = 70}); // alice clawback all confidential balance from dave, which is 200. // dave only has balance in inbox. mptAlice.confidentialClaw( - {.account = alice, .holder = dave, .amt = 200, .proof = "123"}); + {.account = alice, .holder = dave, .amt = 200}); } void @@ -2169,7 +2169,13 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .proof = "123", .err = temBAD_AMOUNT}); - // todo: proof length check + // proof length invalid + mptAlice.confidentialClaw( + {.account = alice, + .holder = bob, + .amt = 10, + .proof = "123", + .err = temMALFORMED}); } } @@ -2223,7 +2229,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = alice, .holder = unknown, .amt = 10, - .proof = "123", .err = tecNO_TARGET}); } @@ -2233,7 +2238,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = alice, .holder = dave, .amt = 10, - .proof = "123", .err = tecOBJECT_NOT_FOUND}); } @@ -2243,7 +2247,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = alice, .holder = carol, .amt = 10, - .proof = "123", .err = tecNO_PERMISSION}); } } @@ -2266,7 +2269,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = alice, .holder = bob, .amt = 10, - .proof = "123", .err = tecNO_PERMISSION}); } @@ -2285,7 +2287,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = alice, .holder = bob, .amt = 10, - .proof = "123", .err = tecNO_PERMISSION}); } @@ -2310,7 +2311,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite jv[sfHolder] = bob.human(); jv[jss::TransactionType] = jss::ConfidentialClawback; jv[sfMPTAmount] = std::to_string(10); - jv[sfZKProof] = "123"; + std::string const dummyProof(196, '0'); + jv[sfZKProof] = dummyProof; jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); env(jv, ter(tecOBJECT_NOT_FOUND)); @@ -2355,7 +2357,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite // clawback should still work mptAlice.confidentialClaw( - {.account = alice, .holder = bob, .amt = 60, .proof = "123"}); + {.account = alice, .holder = bob, .amt = 60}); } // lock globally @@ -2368,7 +2370,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite // clawback should still work mptAlice.confidentialClaw( - {.account = alice, .holder = bob, .amt = 60, .proof = "123"}); + {.account = alice, .holder = bob, .amt = 60}); } // unauthorize should not block clawback @@ -2383,10 +2385,160 @@ class ConfidentialTransfer_test : public beast::unit_test::suite {.account = alice, .holder = bob, .flags = tfMPTUnauthorize}); // clawback should still work mptAlice.confidentialClaw( - {.account = alice, .holder = bob, .amt = 60, .proof = "123"}); + {.account = alice, .holder = bob, .amt = 60}); } - // todo: test zkp verification failure + // insufficient funds, clawback amount exceeding confidential + // outstanding amount + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice = setupAccounts(env, alice, bob); + + mptAlice.confidentialClaw( + {.account = alice, + .holder = bob, + .amt = 10000, + .err = tecINSUFFICIENT_FUNDS}); + } + } + + void + testClawbackProof(FeatureBitset features) + { + testcase("ConfidentialClawback Proof"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + + // lambda function to set up MPT with alice as issuer, bob and carol as + // authorized holders, and fund 1000 mpt to bob and 2000 mpt to carol. + auto setupEnv = [&](Env& env) -> MPTTester { + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create( + {.flags = + tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanPrivacy}); + + for (auto const& [acct, amt] : + {std::pair{bob, 1000}, {carol, 2000}}) + { + mptAlice.authorize({.account = acct}); + mptAlice.pay(alice, acct, amt); + mptAlice.generateKeyPair(acct); + } + + mptAlice.generateKeyPair(alice); + mptAlice.set( + {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + + return mptAlice; + }; + + // lambda function to test a set of bad clawback amounts that should + // return tecBAD_PROOF + auto checkBadProofs = [&](MPTTester& mpt, + Account const& holder, + std::initializer_list amts) { + for (auto const badAmt : amts) + { + mpt.confidentialClaw( + {.account = alice, + .holder = holder, + .amt = badAmt, + .err = tecBAD_PROOF}); + } + }; + + // SCENARIO 1: clawback from inbox only or spending only balances. + // bob converts 500 and merge inbox, + // carol converts 1000, but not merge inbox. + // after setup, bob has 500 in spending, carol has 1000 in inbox. + { + Env env{*this, features}; + auto mptAlice = setupEnv(env); + + // bob converts and merges + mptAlice.convert( + {.account = bob, + .amt = 500, + .proof = "123", + .holderPubKey = mptAlice.getPubKey(bob)}); + mptAlice.mergeInbox({ + .account = bob, + }); + // carol converts without merge + mptAlice.convert( + {.account = carol, + .amt = 1000, + .proof = "123", + .holderPubKey = mptAlice.getPubKey(carol)}); + + // verify proof fails with invalid clawback amount + // bob: 500 in Spending, 0 in Inbox + checkBadProofs( + mptAlice, bob, {1, 10, 70, 100, 110, 200, 499, 501, 600}); + + // carol: 1000 in Inbox, 0 in Spending + checkBadProofs( + mptAlice, carol, {1, 10, 50, 500, 777, 850, 999, 1001, 1200}); + + // clawback with correct amount that passes proof verification + mptAlice.confidentialClaw( + {.account = alice, .holder = bob, .amt = 500}); + mptAlice.confidentialClaw( + {.account = alice, .holder = carol, .amt = 1000}); + } + + // SCENARIO 2: clawback from mixed inbox and spending balances. + // bob converts 300 to confidential and merge inbox, + // carol converts 400 to confidential and merge inbox, + // bob sends 100 to carol, carol sends 100 to bob. + // After setup, bob has 100 in inbox and 200 in spending; + // carol has 100 in inbox and 300 in spending. + { + Env env{*this, features}; + auto mptAlice = setupEnv(env); + + mptAlice.convert( + {.account = bob, + .amt = 300, + .proof = "123", + .holderPubKey = mptAlice.getPubKey(bob)}); + mptAlice.mergeInbox({ + .account = bob, + }); + mptAlice.convert( + {.account = carol, + .amt = 400, + .proof = "123", + .holderPubKey = mptAlice.getPubKey(carol)}); + mptAlice.mergeInbox({ + .account = carol, + }); + mptAlice.send( + {.account = bob, .dest = carol, .amt = 100, .proof = "123"}); + mptAlice.send( + {.account = carol, .dest = bob, .amt = 100, .proof = "123"}); + + // verify proof fails with invalid clawback amount + // bob: 100 in inbox, 200 in spending + checkBadProofs(mptAlice, bob, {1, 10, 50, 100, 200, 299, 301, 400}); + + // proof failure for incorrect amount when clawbacking from carol + // carol: 100 in inbox, 300 in spending + checkBadProofs( + mptAlice, carol, {1, 10, 50, 100, 300, 399, 401, 501}); + + // clawback with correct amount that passes proof verification + mptAlice.confidentialClaw( + {.account = alice, .holder = bob, .amt = 300}); + mptAlice.confidentialClaw( + {.account = alice, .holder = carol, .amt = 400}); + } } void @@ -2611,6 +2763,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite testClawback(features); testClawbackPreflight(features); testClawbackPreclaim(features); + testClawbackProof(features); testDelete(features); diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index c38f27a13d..56049f8643 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -702,6 +702,77 @@ MPTTester::getIssuanceConfidentialBalance() const return 0; } +Buffer +MPTTester::getClawbackProof( + Account const& holder, + std::uint64_t amount, + Buffer const& privateKey, + uint256 const& ctxHash) const +{ + if (!id_) + Throw("MPT has not been created"); + + 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"); + + auto const ciphertextBlob = sleHolder->getFieldVL(sfIssuerEncryptedBalance); + if (ciphertextBlob.size() == 0) + return getDummyProof(); + + auto const pubKeyBlob = sleIssuance->getFieldVL(sfIssuerElGamalPublicKey); + Slice const ciphertext(ciphertextBlob.data(), ciphertextBlob.size()); + Slice const pubKey(pubKeyBlob.data(), pubKeyBlob.size()); + + if (ciphertextBlob.size() != ecGamalEncryptedTotalLength) + Throw("Invalid Ciphertext length"); + + secp256k1_pubkey c1, c2; + auto const ctx = secp256k1Context(); + if (!secp256k1_ec_pubkey_parse( + ctx, &c1, ciphertextBlob.data(), ecGamalEncryptedLength) || + !secp256k1_ec_pubkey_parse( + ctx, + &c2, + ciphertextBlob.data() + ecGamalEncryptedLength, + ecGamalEncryptedLength)) + { + Throw("Invalid Ciphertext"); + } + + secp256k1_pubkey pk; + std::memcpy(pk.data, pubKeyBlob.data(), ecPubKeyLength); + Buffer proof(ecEqualityProofLength); + + if (secp256k1_equality_plaintext_prove( + ctx, + proof.data(), + &pk, + &c2, + &c1, + amount, + privateKey.data(), + ctxHash.data()) != 1) + { + Throw("Proof generation failed"); + } + + return proof; +} + std::optional MPTTester::getEncryptedBalance( Account const& account, @@ -1022,6 +1093,16 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg) if (arg.proof) jv[sfZKProof] = *arg.proof; + else + { + std::uint32_t const seq = env_.seq(account); + uint256 const ctxHash = getClawbackContextHash( + account.id(), seq, *id_, *arg.amt, arg.holder->id()); + Buffer proof = getClawbackProof( + *arg.holder, *arg.amt, getPrivKey(account), ctxHash); + + jv[sfZKProof] = strHex(proof); + } auto const holderPubAmt = getBalance(*arg.holder); auto const prevCOA = getIssuanceConfidentialBalance(); diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 14e46a7b82..93ecee0d03 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -414,6 +414,13 @@ public: std::int64_t getIssuanceOutstandingBalance() const; + Buffer + getClawbackProof( + Account const& holder, + std::uint64_t amount, + Buffer const& privateKey, + uint256 const& txHash) const; + private: using SLEP = SLE::const_pointer; bool diff --git a/src/xrpld/app/tx/detail/ConfidentialClawback.cpp b/src/xrpld/app/tx/detail/ConfidentialClawback.cpp index 29ade4cb90..8af85e6683 100644 --- a/src/xrpld/app/tx/detail/ConfidentialClawback.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialClawback.cpp @@ -32,8 +32,8 @@ ConfidentialClawback::preflight(PreflightContext const& ctx) if (clawAmount == 0 || clawAmount > maxMPTokenAmount) return temBAD_AMOUNT; - // if (ctx.tx[sfZKProof].length() != ecEqualityProofLength) - // return temMALFORMED; + if (ctx.tx[sfZKProof].length() != ecEqualityProofLength) + return temMALFORMED; return tesSUCCESS; } @@ -80,15 +80,34 @@ ConfidentialClawback::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; // Sanity check: claw amount can not exceed confidential outstanding amount - if (ctx.tx[sfMPTAmount] > - (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0)) + auto const amount = ctx.tx[sfMPTAmount]; + if (amount > (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0)) return tecINSUFFICIENT_FUNDS; - // todo: ZKP Verification - // verify the MPT amount to clawback is the holder's confidential balance + auto const ciphertext = (*sleHolderMPToken)[sfIssuerEncryptedBalance]; + auto const pubKeySlice = (*sleIssuance)[sfIssuerElGamalPublicKey]; - // if (!isTesSuccess(terProof)) - // return tecBAD_PROOF; + secp256k1_pubkey c1, c2; + if (!makeEcPair(ciphertext, c1, c2)) + return tecINTERNAL; // LCOV_EXCL_LINE + + secp256k1_pubkey pubKey; + std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength); + + auto const contextHash = getClawbackContextHash( + account, ctx.tx[sfSequence], mptIssuanceID, amount, holder); + + if (secp256k1_equality_plaintext_verify( + secp256k1Context(), + ctx.tx[sfZKProof].data(), + &pubKey, + &c2, + &c1, + amount, + contextHash.data()) != 1) + { + return tecBAD_PROOF; + } return tesSUCCESS; }