Support equality proof for confidential clawback (#6149)

This commit is contained in:
yinyiqian1
2026-01-02 11:48:06 -05:00
committed by GitHub
parent 7c0bd419a4
commit bd3a6e1631
6 changed files with 757 additions and 22 deletions

View File

@@ -10,12 +10,30 @@
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/detail/secp256k1.h>
#include <secp256k1.h>
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

View File

@@ -6,6 +6,39 @@
#include <openssl/sha.h>
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

View File

@@ -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<uint64_t> 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);

View File

@@ -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<std::runtime_error>("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<std::runtime_error>("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<std::runtime_error>("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<std::runtime_error>("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<std::runtime_error>("Proof generation failed");
}
return proof;
}
std::optional<Buffer>
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();

View File

@@ -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

View File

@@ -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;
}