feat: support ConfidentialClawback and add tests (#6023)

This commit is contained in:
yinyiqian1
2025-11-13 14:24:40 -05:00
committed by GitHub
parent c03866bf0f
commit 8365148b5c
8 changed files with 768 additions and 42 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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