mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 08:46:46 +00:00
Support equality proof for confidential clawback (#6149)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user