mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 02:25:52 +00:00
feat: support ConfidentialClawback and add tests (#6023)
This commit is contained in:
@@ -1007,6 +1007,20 @@ TRANSACTION(ttCONFIDENTIAL_SEND, 75, ConfidentialSend,
|
||||
{sfCredentialIDs, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialClawback.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_CLAWBACK, 76, ConfidentialClawback,
|
||||
Delegation::delegatable,
|
||||
featureConfidentialTransfer,
|
||||
noPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfHolder, soeREQUIRED},
|
||||
{sfMPTAmount, soeREQUIRED},
|
||||
{sfZKProof, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
For details, see: https://xrpl.org/amendments.html
|
||||
|
||||
@@ -461,7 +461,7 @@ proveEquality(
|
||||
Serializer s;
|
||||
s.addRaw(txHash.data(), txHash.bytes);
|
||||
s.add32(spendVersion);
|
||||
auto const txContextId = s.getSHA512Half();
|
||||
// auto const txContextId = s.getSHA512Half();
|
||||
|
||||
// todo: support equality
|
||||
// if (secp256k1_equality_verify(
|
||||
@@ -578,7 +578,7 @@ verifyConfidentialSendProof(
|
||||
Serializer s;
|
||||
s.addRaw(txHash.data(), txHash.bytes);
|
||||
s.add32(version);
|
||||
auto const txContextId = s.getSHA512Half();
|
||||
// auto const txContextId = s.getSHA512Half();
|
||||
|
||||
// todo: equality and range proof verification
|
||||
// if (secp256k1_equal_range_verify(
|
||||
|
||||
@@ -863,7 +863,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
Account const carol("carol");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
||||
|
||||
// Basic setup just to have accounts and MPT ID
|
||||
mptAlice.create();
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = carol});
|
||||
@@ -905,7 +904,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
|
||||
// issuer can not be the same as sender
|
||||
mptAlice.send(
|
||||
{.account = alice, // Issuer is sender
|
||||
{.account = alice,
|
||||
.dest = carol,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
@@ -951,7 +950,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
.proof = "123",
|
||||
.senderEncryptedAmt =
|
||||
Buffer(ripple::ecGamalEncryptedTotalLength),
|
||||
.destEncryptedAmt = Buffer(10), // Incorrect length
|
||||
.destEncryptedAmt = Buffer(10),
|
||||
.issuerEncryptedAmt =
|
||||
Buffer(ripple::ecGamalEncryptedTotalLength),
|
||||
.err = temBAD_CIPHERTEXT});
|
||||
@@ -1066,27 +1065,42 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
.account = carol,
|
||||
});
|
||||
|
||||
// // sender does not exist
|
||||
// {
|
||||
// Json::Value jv;
|
||||
// jv[jss::Account] = Account("unknown").human();
|
||||
// jv[jss::Destination] = carol.human();
|
||||
// jv[jss::TransactionType] = jss::ConfidentialSend;
|
||||
// jv[jss::Sequence] = 1;
|
||||
// jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
|
||||
// jv[sfSenderEncryptedAmount.jsonName] =
|
||||
// strHex(Buffer(ripple::ecGamalEncryptedTotalLength));
|
||||
// jv[sfDestinationEncryptedAmount.jsonName] =
|
||||
// strHex(Buffer(ripple::ecGamalEncryptedTotalLength));
|
||||
// jv[sfIssuerEncryptedAmount.jsonName] =
|
||||
// strHex(Buffer(ripple::ecGamalEncryptedTotalLength));
|
||||
// jv[sfZKProof.jsonName] = "123";
|
||||
// env(jv, ter(terNO_ACCOUNT));
|
||||
// env.close();
|
||||
// }
|
||||
|
||||
auto const ciphertextHex = generatePlaceholderCiphertext();
|
||||
|
||||
// issuance not found
|
||||
{
|
||||
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({.flags = tfMPTCanTransfer});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = carol});
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.set(
|
||||
{.account = alice, .pubKey = mptAlice.getPubKey(alice)});
|
||||
|
||||
// destroy the issuance
|
||||
mptAlice.destroy();
|
||||
env.close();
|
||||
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = bob.human();
|
||||
jv[jss::Destination] = carol.human();
|
||||
jv[jss::TransactionType] = jss::ConfidentialSend;
|
||||
jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
|
||||
|
||||
auto const encryptedAmt = strHex(ciphertextHex);
|
||||
jv[sfSenderEncryptedAmount] = encryptedAmt;
|
||||
jv[sfDestinationEncryptedAmount] = encryptedAmt;
|
||||
jv[sfIssuerEncryptedAmount] = encryptedAmt;
|
||||
jv[sfZKProof] = "123";
|
||||
|
||||
env(jv, ter(tecOBJECT_NOT_FOUND));
|
||||
}
|
||||
|
||||
// destination does not exist
|
||||
{
|
||||
Account const unknown("unknown");
|
||||
@@ -1095,8 +1109,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
.dest = unknown,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.issuerEncryptedAmt = ciphertextHex,
|
||||
.destEncryptedAmt = ciphertextHex,
|
||||
.issuerEncryptedAmt = ciphertextHex,
|
||||
.err = tecNO_TARGET});
|
||||
}
|
||||
|
||||
@@ -1984,6 +1998,405 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testClawback(FeatureBitset features)
|
||||
{
|
||||
testcase("test ConfidentialClawback");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
Account const dave("dave");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave}});
|
||||
|
||||
mptAlice.create(
|
||||
{.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
mptAlice.authorize({.account = carol});
|
||||
mptAlice.pay(alice, carol, 200);
|
||||
mptAlice.authorize({.account = dave});
|
||||
mptAlice.pay(alice, dave, 300);
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(carol);
|
||||
mptAlice.generateKeyPair(dave);
|
||||
mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)});
|
||||
|
||||
// setup bob.
|
||||
// after setup, bob's spending balance is 60, inbox balance is 0.
|
||||
{
|
||||
// bob converts 60 to confidential
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 60,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob)});
|
||||
|
||||
// bob merge inbox
|
||||
mptAlice.mergeInbox({
|
||||
.account = bob,
|
||||
});
|
||||
}
|
||||
|
||||
// setup carol.
|
||||
// after setup, carol's spending balance is 120, inbox balance is 0.
|
||||
{
|
||||
// carol converts 120 to confidential
|
||||
mptAlice.convert(
|
||||
{.account = carol,
|
||||
.amt = 120,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(carol)});
|
||||
|
||||
// carol merge inbox
|
||||
mptAlice.mergeInbox({
|
||||
.account = carol,
|
||||
});
|
||||
}
|
||||
|
||||
// setup dave.
|
||||
// dave will not merge inbox.
|
||||
// after setup, dave's inbox balance is 200, spending balance is 0.
|
||||
mptAlice.convert(
|
||||
{.account = dave,
|
||||
.amt = 200,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(dave)});
|
||||
|
||||
// setup: carol confidential send 50 to bob.
|
||||
// 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"});
|
||||
|
||||
// alice clawback all confidential balance from bob, 110 in total.
|
||||
// bob has balance in both inbox and spending. These balances should
|
||||
// become zero after clawback, which is verified in the confidentialClaw
|
||||
// function.
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice, .holder = bob, .amt = 110, .proof = "123"});
|
||||
|
||||
// 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"});
|
||||
|
||||
// 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"});
|
||||
}
|
||||
|
||||
void
|
||||
testClawbackPreflight(FeatureBitset features)
|
||||
{
|
||||
testcase("test ConfidentialClawback Preflight");
|
||||
using namespace test::jtx;
|
||||
|
||||
// test feature disabled
|
||||
{
|
||||
Env env{*this, features - featureConfidentialTransfer};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create();
|
||||
mptAlice.authorize({.account = bob});
|
||||
env.close();
|
||||
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice,
|
||||
.holder = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.err = temDISABLED});
|
||||
}
|
||||
|
||||
// test malformed
|
||||
{
|
||||
// set up
|
||||
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();
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = carol});
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(carol);
|
||||
mptAlice.set(
|
||||
{.account = alice, .pubKey = mptAlice.getPubKey(alice)});
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
mptAlice.pay(alice, carol, 50);
|
||||
env.close();
|
||||
|
||||
// only issuer can clawback
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = carol,
|
||||
.holder = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.err = temMALFORMED});
|
||||
|
||||
// invalid issuance ID, whose issuer is not alice
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[sfHolder] = bob.human();
|
||||
jv[jss::TransactionType] = jss::ConfidentialClawback;
|
||||
jv[sfMPTAmount] = std::to_string(10);
|
||||
jv[sfZKProof] = "123";
|
||||
|
||||
// wrong issuance ID
|
||||
jv[sfMPTokenIssuanceID] =
|
||||
"00000004AE123A8556F3CF91154711376AFB0F894F832B3E";
|
||||
|
||||
env(jv, ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// issuer cannot clawback from self
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice,
|
||||
.holder = alice,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.err = temMALFORMED});
|
||||
|
||||
// invalid amount
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice,
|
||||
.holder = bob,
|
||||
.amt = 0,
|
||||
.proof = "123",
|
||||
.err = temBAD_AMOUNT});
|
||||
|
||||
// todo: proof length check
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testClawbackPreclaim(FeatureBitset features)
|
||||
{
|
||||
testcase("Clawback Preclaim Errors");
|
||||
using namespace test::jtx;
|
||||
|
||||
{
|
||||
// set up, alice is the issuer, bob and carol are authorized
|
||||
// holders. dave is not authorized. bob has confidential balance,
|
||||
// carol does not.
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
Account const dave("dave");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave}});
|
||||
|
||||
mptAlice.create(
|
||||
{.flags =
|
||||
tfMPTCanTransfer | tfMPTCanClawback | tfMPTRequireAuth});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = alice, .holder = bob});
|
||||
mptAlice.authorize({.account = carol});
|
||||
mptAlice.authorize({.account = alice, .holder = carol});
|
||||
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
mptAlice.pay(alice, carol, 50);
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(carol);
|
||||
mptAlice.set(
|
||||
{.account = alice, .pubKey = mptAlice.getPubKey(alice)});
|
||||
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 60,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
mptAlice.mergeInbox({
|
||||
.account = bob,
|
||||
});
|
||||
|
||||
// holder does not exist
|
||||
{
|
||||
Account const unknown("unknown");
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice,
|
||||
.holder = unknown,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.err = tecNO_TARGET});
|
||||
}
|
||||
|
||||
// dave does not hold mpt at all, no MPT object
|
||||
{
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice,
|
||||
.holder = dave,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.err = tecOBJECT_NOT_FOUND});
|
||||
}
|
||||
|
||||
// carol has no confidential balance
|
||||
{
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice,
|
||||
.holder = carol,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.err = tecNO_PERMISSION});
|
||||
}
|
||||
}
|
||||
|
||||
// lsfMPTCanClawback not set
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create({.flags = tfMPTCanTransfer});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.set(
|
||||
{.account = alice, .pubKey = mptAlice.getPubKey(alice)});
|
||||
env.close();
|
||||
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice,
|
||||
.holder = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.err = tecNO_PERMISSION});
|
||||
}
|
||||
|
||||
// no issuer key
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create({.flags = tfMPTCanClawback});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.generateKeyPair(alice);
|
||||
env.close();
|
||||
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice,
|
||||
.holder = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.err = tecNO_PERMISSION});
|
||||
}
|
||||
|
||||
// issuance not found
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
mptAlice.create({.flags = tfMPTCanClawback});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.set(
|
||||
{.account = alice, .pubKey = mptAlice.getPubKey(alice)});
|
||||
|
||||
// destroy the issuance
|
||||
mptAlice.destroy();
|
||||
env.close();
|
||||
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[sfHolder] = bob.human();
|
||||
jv[jss::TransactionType] = jss::ConfidentialClawback;
|
||||
jv[sfMPTAmount] = std::to_string(10);
|
||||
jv[sfZKProof] = "123";
|
||||
jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
|
||||
|
||||
env(jv, ter(tecOBJECT_NOT_FOUND));
|
||||
}
|
||||
|
||||
// helper function to set up accounts to test lock and unauthorize
|
||||
// cases. after set up, bob has confidential balance 60 in spending.
|
||||
auto setupAccounts = [&](Env& env,
|
||||
Account const& alice,
|
||||
Account const& bob) -> MPTTester {
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create(
|
||||
{.flags = tfMPTCanTransfer | tfMPTCanClawback |
|
||||
tfMPTRequireAuth | tfMPTCanLock});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = alice, .holder = bob});
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.set(
|
||||
{.account = alice, .pubKey = mptAlice.getPubKey(alice)});
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 60,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob)});
|
||||
mptAlice.mergeInbox({
|
||||
.account = bob,
|
||||
});
|
||||
|
||||
return mptAlice;
|
||||
};
|
||||
|
||||
// lock should not block clawback. lock bob individually
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice = setupAccounts(env, alice, bob);
|
||||
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
||||
|
||||
// clawback should still work
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice, .holder = bob, .amt = 60, .proof = "123"});
|
||||
}
|
||||
|
||||
// lock globally
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice = setupAccounts(env, alice, bob);
|
||||
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
||||
|
||||
// clawback should still work
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice, .holder = bob, .amt = 60, .proof = "123"});
|
||||
}
|
||||
|
||||
// unauthorize should not block clawback
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice = setupAccounts(env, alice, bob);
|
||||
|
||||
// unauthorize bob
|
||||
mptAlice.authorize(
|
||||
{.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
|
||||
// clawback should still work
|
||||
mptAlice.confidentialClaw(
|
||||
{.account = alice, .holder = bob, .amt = 60, .proof = "123"});
|
||||
}
|
||||
|
||||
// todo: test zkp verification failure
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
@@ -2003,6 +2416,11 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
testSendPreclaim(features);
|
||||
testSendDepositPreauth(features);
|
||||
|
||||
// ConfidentialClawback
|
||||
testClawback(features);
|
||||
testClawbackPreflight(features);
|
||||
testClawbackPreclaim(features);
|
||||
|
||||
testDelete(features);
|
||||
|
||||
testConvertBack(features);
|
||||
|
||||
@@ -373,8 +373,10 @@ MPTTester::checkDomainID(std::optional<uint256> expected) const
|
||||
MPTTester::printMPT(Account const& holder_) const
|
||||
{
|
||||
return forObject(
|
||||
[&](SLEP const& sle) -> bool { std::cout << "\n"
|
||||
<< sle->getJson(); },
|
||||
[&](SLEP const& sle) -> bool {
|
||||
std::cout << "\n" << sle->getJson();
|
||||
return true;
|
||||
},
|
||||
holder_);
|
||||
}
|
||||
|
||||
@@ -678,9 +680,6 @@ MPTTester::convert(MPTConvert const& arg)
|
||||
uint64_t postSpendingBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
||||
|
||||
// std::cout << "\n postIssuerBalance is " << postIssuerBalance << '\n';
|
||||
// std::cout << "\n postInboxBalance is " << postInboxBalance << '\n';
|
||||
|
||||
// spending balance should not change
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return postSpendingBalance == prevSpendingBalance;
|
||||
@@ -746,26 +745,24 @@ MPTTester::send(MPTConfidentialSend const& arg)
|
||||
|
||||
// Generate the encrypted amounts if not provided
|
||||
if (arg.senderEncryptedAmt)
|
||||
jv[sfSenderEncryptedAmount.jsonName] = strHex(*arg.senderEncryptedAmt);
|
||||
jv[sfSenderEncryptedAmount] = strHex(*arg.senderEncryptedAmt);
|
||||
else
|
||||
jv[sfSenderEncryptedAmount.jsonName] =
|
||||
jv[sfSenderEncryptedAmount] =
|
||||
strHex(encryptAmount(*arg.account, *arg.amt));
|
||||
|
||||
if (arg.destEncryptedAmt)
|
||||
jv[sfDestinationEncryptedAmount.jsonName] =
|
||||
strHex(*arg.destEncryptedAmt);
|
||||
jv[sfDestinationEncryptedAmount] = strHex(*arg.destEncryptedAmt);
|
||||
else
|
||||
jv[sfDestinationEncryptedAmount.jsonName] =
|
||||
jv[sfDestinationEncryptedAmount] =
|
||||
strHex(encryptAmount(*arg.dest, *arg.amt));
|
||||
|
||||
if (arg.issuerEncryptedAmt)
|
||||
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
|
||||
jv[sfIssuerEncryptedAmount] = strHex(*arg.issuerEncryptedAmt);
|
||||
else
|
||||
jv[sfIssuerEncryptedAmount.jsonName] =
|
||||
strHex(encryptAmount(issuer_, *arg.amt));
|
||||
jv[sfIssuerEncryptedAmount] = strHex(encryptAmount(issuer_, *arg.amt));
|
||||
|
||||
if (arg.proof)
|
||||
jv[sfZKProof.jsonName] = *arg.proof;
|
||||
jv[sfZKProof] = *arg.proof;
|
||||
|
||||
if (arg.credentials)
|
||||
{
|
||||
@@ -856,6 +853,68 @@ MPTTester::send(MPTConfidentialSend const& arg)
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::confidentialClaw(MPTConfidentialClawback const& arg)
|
||||
{
|
||||
Json::Value jv;
|
||||
auto const account = arg.account ? *arg.account : issuer_;
|
||||
jv[sfAccount] = account.human();
|
||||
|
||||
if (arg.holder)
|
||||
jv[sfHolder] = arg.holder->human();
|
||||
else
|
||||
Throw<std::runtime_error>("Holder not specified");
|
||||
|
||||
jv[jss::TransactionType] = jss::ConfidentialClawback;
|
||||
if (arg.id)
|
||||
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
|
||||
else if (id_)
|
||||
jv[sfMPTokenIssuanceID] = to_string(*id_);
|
||||
else
|
||||
Throw<std::runtime_error>("MPT has not been created");
|
||||
|
||||
if (arg.amt)
|
||||
jv[sfMPTAmount] = std::to_string(*arg.amt);
|
||||
|
||||
if (arg.proof)
|
||||
jv[sfZKProof] = *arg.proof;
|
||||
|
||||
auto const holderPubAmt = getBalance(*arg.holder);
|
||||
auto const prevCOA = getIssuanceConfidentialBalance();
|
||||
auto const prevOA = getIssuanceOutstandingBalance();
|
||||
|
||||
if (submit(arg, jv) == tesSUCCESS)
|
||||
{
|
||||
auto const postCOA = getIssuanceConfidentialBalance();
|
||||
auto const postOA = getIssuanceOutstandingBalance();
|
||||
|
||||
// Verify holder's public balance is unchanged
|
||||
env_.require(mptbalance(*this, *arg.holder, holderPubAmt));
|
||||
|
||||
// Verify COA and OA are reduced correctly
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevCOA >= *arg.amt && postCOA == prevCOA - *arg.amt;
|
||||
}));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevOA >= *arg.amt && postOA == prevOA - *arg.amt;
|
||||
}));
|
||||
|
||||
// Verify holder's confidential balances are zeroed out
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return getDecryptedBalance(*arg.holder, HOLDER_ENCRYPTED_INBOX) ==
|
||||
0;
|
||||
}));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return getDecryptedBalance(
|
||||
*arg.holder, HOLDER_ENCRYPTED_SPENDING) == 0;
|
||||
}));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return getDecryptedBalance(*arg.holder, ISSUER_ENCRYPTED_BALANCE) ==
|
||||
0;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::generateKeyPair(Account const& account)
|
||||
{
|
||||
|
||||
@@ -197,6 +197,7 @@ struct MPTConfidentialSend
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
struct MPTConvertBack
|
||||
{
|
||||
std::optional<Account> account = std::nullopt;
|
||||
@@ -211,6 +212,19 @@ struct MPTConvertBack
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
struct MPTConfidentialClawback
|
||||
{
|
||||
std::optional<Account> account = std::nullopt;
|
||||
std::optional<Account> holder = std::nullopt;
|
||||
std::optional<MPTID> id = std::nullopt;
|
||||
std::optional<std::uint64_t> amt = std::nullopt;
|
||||
std::optional<std::string> proof = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
class MPTTester
|
||||
{
|
||||
Env& env_;
|
||||
@@ -254,6 +268,10 @@ public:
|
||||
void
|
||||
convertBack(MPTConvertBack const& arg = MPTConvertBack{});
|
||||
|
||||
void
|
||||
confidentialClaw(
|
||||
MPTConfidentialClawback const& arg = MPTConfidentialClawback{});
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkDomainID(std::optional<uint256> expected) const;
|
||||
|
||||
|
||||
169
src/xrpld/app/tx/detail/ConfidentialClawback.cpp
Normal file
169
src/xrpld/app/tx/detail/ConfidentialClawback.cpp
Normal file
@@ -0,0 +1,169 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/DelegateUtils.h>
|
||||
#include <xrpld/app/tx/detail/ConfidentialClawback.h>
|
||||
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
NotTEC
|
||||
ConfidentialClawback::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureConfidentialTransfer))
|
||||
return temDISABLED;
|
||||
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer();
|
||||
|
||||
// Only issuer can clawback
|
||||
if (account != issuer)
|
||||
return temMALFORMED;
|
||||
|
||||
// Cannot clawback from self
|
||||
if (account == ctx.tx[sfHolder])
|
||||
return temMALFORMED;
|
||||
|
||||
auto const clawAmount = ctx.tx[sfMPTAmount];
|
||||
if (clawAmount == 0 || clawAmount > maxMPTokenAmount)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
// if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
|
||||
// return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialClawback::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// Check if sender account exists
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
if (!ctx.view.exists(keylet::account(account)))
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
// Check if holder account exists
|
||||
auto const holder = ctx.tx[sfHolder];
|
||||
if (!ctx.view.exists(keylet::account(holder)))
|
||||
return tecNO_TARGET;
|
||||
|
||||
// Check if MPT issuance exists
|
||||
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// Sanity check: issuer must be the same as account
|
||||
if (sleIssuance->getAccountID(sfIssuer) != account)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// Check if issuance has issuer ElGamal public key
|
||||
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Check if clawback is allowed
|
||||
if (!sleIssuance->isFlag(lsfMPTCanClawback))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Check holder's MPToken
|
||||
auto const sleHolderMPToken =
|
||||
ctx.view.read(keylet::mptoken(mptIssuanceID, holder));
|
||||
if (!sleHolderMPToken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// Check if holder has confidential balances to claw back
|
||||
if (!sleHolderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Sanity check: claw amount can not exceed confidential outstanding amount
|
||||
if (ctx.tx[sfMPTAmount] >
|
||||
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0))
|
||||
return temBAD_AMOUNT; // LCOV_EXCL_LINE
|
||||
|
||||
// todo: ZKP Verification
|
||||
// verify the MPT amount to clawback is the holder's confidential balance
|
||||
|
||||
// if (!isTesSuccess(terProof))
|
||||
// return tecBAD_PROOF;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialClawback::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto const holder = ctx_.tx[sfHolder];
|
||||
|
||||
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
|
||||
auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder));
|
||||
|
||||
if (!sleIssuance || !sleHolderMPToken)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const clawAmount = ctx_.tx[sfMPTAmount];
|
||||
|
||||
Slice const holderPubKey = (*sleHolderMPToken)[sfHolderElGamalPublicKey];
|
||||
Slice const issuerPubKey = (*sleIssuance)[sfIssuerElGamalPublicKey];
|
||||
|
||||
// Encrypt zero amount
|
||||
Buffer encZeroForHolder;
|
||||
Buffer encZeroForIssuer;
|
||||
try
|
||||
{
|
||||
encZeroForHolder =
|
||||
encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID);
|
||||
|
||||
encZeroForIssuer =
|
||||
encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(ctx_.journal.error())
|
||||
<< "Clawback: Failed to generate canonical zero: " << e.what();
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
// Set holder's confidential balances to encrypted zero
|
||||
(*sleHolderMPToken)[sfConfidentialBalanceInbox] = encZeroForHolder;
|
||||
(*sleHolderMPToken)[sfConfidentialBalanceSpending] = encZeroForHolder;
|
||||
(*sleHolderMPToken)[sfIssuerEncryptedBalance] = encZeroForIssuer;
|
||||
(*sleHolderMPToken)[sfConfidentialBalanceVersion] = 0;
|
||||
|
||||
// Decrease Global Confidential Outstanding Amount
|
||||
auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount];
|
||||
(*sleIssuance)[sfConfidentialOutstandingAmount] = oldCOA - clawAmount;
|
||||
|
||||
// Decrease Global Total Outstanding Amount
|
||||
auto const oldOA = (*sleIssuance)[sfOutstandingAmount];
|
||||
(*sleIssuance)[sfOutstandingAmount] = oldOA - clawAmount;
|
||||
|
||||
view().update(sleHolderMPToken);
|
||||
view().update(sleIssuance);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
} // namespace ripple
|
||||
48
src/xrpld/app/tx/detail/ConfidentialClawback.h
Normal file
48
src/xrpld/app/tx/detail/ConfidentialClawback.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_CONFIDENTIALCLAWSBACK_H_INCLUDED
|
||||
#define RIPPLE_TX_CONFIDENTIALCLAWSBACK_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ConfidentialClawback : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialClawback(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -113,9 +113,9 @@ ConfidentialConvert::preclaim(PreclaimContext const& ctx)
|
||||
ctx.tx.isFieldPresent(sfHolderElGamalPublicKey))
|
||||
return tecDUPLICATE;
|
||||
|
||||
auto const holderPubKey = ctx.tx.isFieldPresent(sfHolderElGamalPublicKey)
|
||||
? ctx.tx[sfHolderElGamalPublicKey]
|
||||
: (*sleMptoken)[sfHolderElGamalPublicKey];
|
||||
// auto const holderPubKey = ctx.tx.isFieldPresent(sfHolderElGamalPublicKey)
|
||||
// ? ctx.tx[sfHolderElGamalPublicKey]
|
||||
// : (*sleMptoken)[sfHolderElGamalPublicKey];
|
||||
|
||||
// todo: check zkproof/well formed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user