Basic ConvertBack test (#5979)

This commit is contained in:
Shawn Xie
2025-10-31 11:46:24 -04:00
committed by GitHub
parent 3af758145c
commit 44d885e39b
7 changed files with 240 additions and 49 deletions

View File

@@ -1360,6 +1360,56 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
// todo: test with convert back and delete
}
void
testConvertBack(FeatureBitset features)
{
testcase("Convert back");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer | tfMPTCanLock});
mptAlice.authorize({.account = bob});
env.close();
mptAlice.pay(alice, bob, 100);
env.close();
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)});
mptAlice.generateKeyPair(bob);
mptAlice.convert({
.account = bob,
.amt = 40,
.proof = "123",
.holderPubKey = mptAlice.getPubKey(bob),
});
mptAlice.mergeInbox({
.account = bob,
});
mptAlice.convertBack({
.account = bob,
.amt = 30,
.proof = "123",
});
// mptAlice.convertBack({
// .account = bob,
// .amt = 10,
// .proof = "123",
// });
}
void
testWithFeats(FeatureBitset features)
{
@@ -1379,6 +1429,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
testSendPreclaim(features);
testDelete(features);
testConvertBack(features);
}
public:

View File

@@ -980,6 +980,91 @@ MPTTester::getIssuanceOutstandingBalance() const
return (*sle)[sfOutstandingAmount];
}
void
MPTTester::convertBack(MPTConvertBack const& arg)
{
Json::Value jv;
if (arg.account)
jv[sfAccount] = arg.account->human();
else
Throw<std::runtime_error>("Account not specified");
jv[jss::TransactionType] = jss::ConfidentialConvertBack;
if (arg.id)
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
else
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
jv[sfMPTokenIssuanceID] = to_string(*id_);
}
if (arg.amt)
jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
if (arg.holderEncryptedAmt)
jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt);
else
jv[sfHolderEncryptedAmount.jsonName] =
strHex(encryptAmount(*arg.account, *arg.amt));
if (arg.issuerEncryptedAmt)
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
else
jv[sfIssuerEncryptedAmount.jsonName] =
strHex(encryptAmount(issuer_, *arg.amt));
if (arg.proof)
jv[sfZKProof.jsonName] = *arg.proof;
auto const holderAmt = getBalance(*arg.account);
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
uint64_t prevInboxBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t prevSpendingBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
uint64_t prevIssuerBalance =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
if (submit(arg, jv) == tesSUCCESS)
{
auto const postConfidentialOutstanding =
getIssuanceConfidentialBalance();
env_.require(mptbalance(*this, *arg.account, holderAmt + *arg.amt));
env_.require(requireAny([&]() -> bool {
return prevConfidentialOutstanding - *arg.amt ==
postConfidentialOutstanding;
}));
uint64_t postInboxBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t postIssuerBalance =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
uint64_t postSpendingBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
// inbox balance should not change
env_.require(requireAny(
[&]() -> bool { return postInboxBalance == prevInboxBalance; }));
// issuer's encrypted balance is updated correctly
env_.require(requireAny([&]() -> bool {
return prevIssuerBalance - *arg.amt == postIssuerBalance;
}));
// holder's spending balance is updated correctly
env_.require(requireAny([&]() -> bool {
return prevSpendingBalance - *arg.amt == postSpendingBalance;
}));
// sum of holder's inbox and spending balance should equal to issuer's
// encrypted balance
env_.require(requireAny([&]() -> bool {
return postInboxBalance + postSpendingBalance == postIssuerBalance;
}));
}
}
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -196,6 +196,19 @@ struct MPTConfidentialSend
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<TER> err = std::nullopt;
};
struct MPTConvertBack
{
std::optional<Account> account = 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<Buffer> holderEncryptedAmt = std::nullopt;
std::optional<Buffer> issuerEncryptedAmt = 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
{
@@ -237,6 +250,9 @@ public:
void
send(MPTConfidentialSend const& arg = MPTConfidentialSend{});
void
convertBack(MPTConvertBack const& arg = MPTConvertBack{});
[[nodiscard]] bool
checkDomainID(std::optional<uint256> expected) const;

View File

@@ -70,6 +70,11 @@ ConfidentialConvert::preclaim(PreclaimContext const& ctx)
if (sleIssuance->isFlag(lsfMPTNoConfidentialTransfer))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
// issuer has not uploaded their pub key yet
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
return tecNO_PERMISSION;

View File

@@ -19,6 +19,7 @@
#include <xrpld/app/tx/detail/ConfidentialConvertBack.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -43,13 +44,17 @@ ConfidentialConvertBack::preflight(PreflightContext const& ctx)
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temMALFORMED;
if (ctx.tx[sfMPTAmount] == 0)
if (ctx.tx[sfMPTAmount] == 0 || ctx.tx[sfMPTAmount] > maxMPTokenAmount)
return temMALFORMED;
if (!isValidCiphertext(ctx.tx[sfHolderEncryptedAmount]) ||
!isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
return temBAD_CIPHERTEXT;
// todo: update with correct size of proof since it might also contain range
// proof
if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
return temMALFORMED;
// if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
// return temMALFORMED;
return tesSUCCESS;
}
@@ -66,6 +71,11 @@ ConfidentialConvertBack::preclaim(PreclaimContext const& ctx)
if (sleIssuance->isFlag(lsfMPTNoConfidentialTransfer))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleMptoken = ctx.view.read(
keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
if (!sleMptoken)
@@ -86,27 +96,41 @@ ConfidentialConvertBack::preclaim(PreclaimContext const& ctx)
return tecINSUFFICIENT_FUNDS;
}
// todo: need addtional parsing, the proof should contain multiple proofs
auto checkEqualityProof = [&](auto const& encryptedAmount,
auto const& pubKey) -> TER {
return proveEquality(
ctx.tx[sfZKProof],
encryptedAmount,
pubKey,
ctx.tx[sfMPTAmount],
ctx.tx.getTransactionID(),
(*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0));
};
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const account = ctx.tx[sfAccount];
if (!isTesSuccess(checkEqualityProof(
ctx.tx[sfHolderEncryptedAmount],
(*sleMptoken)[sfHolderElGamalPublicKey])) ||
!isTesSuccess(checkEqualityProof(
ctx.tx[sfIssuerEncryptedAmount],
(*sleIssuance)[sfIssuerElGamalPublicKey])))
{
return tecBAD_PROOF;
}
// Check lock
MPTIssue const mptIssue(mptIssuanceID);
if (auto const ter = checkFrozen(ctx.view, account, mptIssue);
ter != tesSUCCESS)
return ter;
// Check auth
if (auto const ter = requireAuth(ctx.view, mptIssue, account);
!isTesSuccess(ter))
return ter;
// todo: need addtional parsing, the proof should contain multiple proofs
// auto checkEqualityProof = [&](auto const& encryptedAmount,
// auto const& pubKey) -> TER {
// return proveEquality(
// ctx.tx[sfZKProof],
// encryptedAmount,
// pubKey,
// ctx.tx[sfMPTAmount],
// ctx.tx.getTransactionID(),
// (*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0));
// };
// if (!isTesSuccess(checkEqualityProof(
// ctx.tx[sfHolderEncryptedAmount],
// (*sleMptoken)[sfHolderElGamalPublicKey])) ||
// !isTesSuccess(checkEqualityProof(
// ctx.tx[sfIssuerEncryptedAmount],
// (*sleIssuance)[sfIssuerElGamalPublicKey])))
// {
// return tecBAD_PROOF;
// }
// todo: also check range proof that
// sfHolderEncryptedAmount <= sfConfidentialBalanceSpending AND
@@ -139,32 +163,31 @@ ConfidentialConvertBack::doApply()
(*sleMptoken)[sfConfidentialBalanceVersion] =
(*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
// todo: support homomophic sub
// // homomorphically subtract holder's encrypted balance
// {
// Buffer res(ecGamalEncryptedTotalLength);
// if (TER const ter = homomorphicSub(
// (*sleMptoken)[sfConfidentialBalanceSpending],
// ctx_.tx[sfHolderEncryptedAmount],
// res);
// isTesSuccess(ter))
// return tecINTERNAL;
// homomorphically subtract holder's encrypted balance
{
Buffer res(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicSubtract(
(*sleMptoken)[sfConfidentialBalanceSpending],
ctx_.tx[sfHolderEncryptedAmount],
res);
!isTesSuccess(ter))
return tecINTERNAL;
// (*sleMptoken)[sfConfidentialBalanceSpending] = res;
// }
(*sleMptoken)[sfConfidentialBalanceSpending] = res;
}
// // homomorphically subtract issuer's encrypted balance
// {
// Buffer res(ecGamalEncryptedTotalLength);
// if (TER const ter = homomorphicSub(
// (*sleMptoken)[sfIssuerEncryptedBalance],
// ctx_.tx[sfIssuerEncryptedAmount],
// res);
// isTesSuccess(ter))
// return tecINTERNAL;
// homomorphically subtract issuer's encrypted balance
{
Buffer res(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicSubtract(
(*sleMptoken)[sfIssuerEncryptedBalance],
ctx_.tx[sfIssuerEncryptedAmount],
res);
!isTesSuccess(ter))
return tecINTERNAL;
// (*sleMptoken)[sfIssuerEncryptedBalance] = res;
// }
(*sleMptoken)[sfIssuerEncryptedBalance] = res;
}
view().update(sleIssuance);
view().update(sleMptoken);

View File

@@ -52,6 +52,11 @@ ConfidentialMergeInbox::preclaim(PreclaimContext const& ctx)
if (sleIssuance->isFlag(lsfMPTNoConfidentialTransfer))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleMptoken = ctx.view.read(
keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
if (!sleMptoken)

View File

@@ -97,6 +97,11 @@ ConfidentialSend::preclaim(PreclaimContext const& ctx)
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the
// issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
return tefINTERNAL; // LCOV_EXCL_LINE
// Check sender's MPToken
auto const sleSenderMPToken =
ctx.view.read(keylet::mptoken(mptIssuanceID, account));
@@ -178,7 +183,7 @@ ConfidentialSend::doApply()
Slice const destEc = ctx_.tx[sfDestinationEncryptedAmount];
Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
// Substract from sender's spending balance
// Subtract from sender's spending balance
{
Slice const curSpending = (*sleSender)[sfConfidentialBalanceSpending];
Buffer newSpending(ecGamalEncryptedTotalLength);
@@ -191,7 +196,7 @@ ConfidentialSend::doApply()
(*sleSender)[sfConfidentialBalanceSpending] = newSpending;
}
// Substract from issuer's balance
// Subtract from issuer's balance
{
Slice const curIssuerEnc = (*sleSender)[sfIssuerEncryptedBalance];
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
@@ -206,7 +211,7 @@ ConfidentialSend::doApply()
// Increment version
(*sleSender)[sfConfidentialBalanceVersion] =
(*sleSender)[sfConfidentialBalanceVersion] + 1;
(*sleSender)[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
// Add to destination's inbox balance
{