Files
rippled/src/test/jtx/impl/mpt.cpp
2026-06-26 00:57:23 -04:00

2544 lines
81 KiB
C++

#include <test/jtx/mpt.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/credentials.h>
#include <test/jtx/owners.h>
#include <test/jtx/pay.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <utility/mpt_utility.h>
#include <mpt_protocol.h>
#include <secp256k1.h>
#include <secp256k1_mpt.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <functional>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>
namespace xrpl::test::jtx {
namespace {
static constexpr std::uint64_t kElGamalDecryptRangeLow = 0;
static constexpr std::uint64_t kElGamalDecryptRangeHigh = 3000;
/**
* @brief Returns a reference to the value held by an optional, throwing if it
* is not an optional.
*
* @param opt The optional to unwrap.
* @param what Description used in the thrown exception if opt is empty.
* @return A const reference to the contained value.
*/
template <class T>
[[nodiscard]] T const&
requireValue(std::optional<T> const& opt, char const* what)
{
if (!opt)
Throw<std::logic_error>(what);
return *opt;
}
/**
* @brief Helper function to convert a PedersenProofParams into the C library struct.
*
* @param params The Pedersen commitment proof parameters.
* @return The equivalent mpt_pedersen_proof_params for use with the C library.
*/
mpt_pedersen_proof_params
makePedersenParams(PedersenProofParams const& params)
{
mpt_pedersen_proof_params res{};
std::memcpy(
res.pedersen_commitment, params.pedersenCommitment.data(), kMPT_PEDERSEN_COMMIT_SIZE);
res.amount = params.amt;
std::memcpy(res.ciphertext, params.encryptedAmt.data(), kMPT_ELGAMAL_TOTAL_SIZE);
std::memcpy(res.blinding_factor, params.blindingFactor.data(), kMPT_BLINDING_FACTOR_SIZE);
return res;
}
} // namespace
struct MPTSetFlagMapping
{
std::uint32_t setFlag;
std::uint32_t ledgerFlag;
};
static constexpr std::array<MPTSetFlagMapping, 6> mptSetFlagMappings = {{
{.setFlag = tmfMPTSetCanLock, .ledgerFlag = lsfMPTCanLock},
{.setFlag = tmfMPTSetRequireAuth, .ledgerFlag = lsfMPTRequireAuth},
{.setFlag = tmfMPTSetCanEscrow, .ledgerFlag = lsfMPTCanEscrow},
{.setFlag = tmfMPTSetCanClawback, .ledgerFlag = lsfMPTCanClawback},
{.setFlag = tmfMPTSetCanTrade, .ledgerFlag = lsfMPTCanTrade},
{.setFlag = tmfMPTSetCanTransfer, .ledgerFlag = lsfMPTCanTransfer},
}};
void
MptFlags::operator()(Env& env) const
{
env.test.expect(tester_.checkFlags(flags_, holder_));
}
void
MptBalance::operator()(Env& env) const
{
env.test.expect(amount_ == tester_.getBalance(account_));
}
void
RequireAny::operator()(Env& env) const
{
env.test.expect(cb_());
}
std::unordered_map<std::string, Account>
MPTTester::makeHolders(std::vector<Account> const& holders)
{
std::unordered_map<std::string, Account> accounts;
for (auto const& h : holders)
{
if (accounts.find(h.human()) != accounts.cend())
Throw<std::runtime_error>("Duplicate holder");
accounts.emplace(h.human(), h);
}
return accounts;
}
MPTTester::MPTTester(Env& env, Account issuer, MPTInit const& arg)
: env_(env)
, issuer_(std::move(issuer))
, holders_(makeHolders(arg.holders))
, auditor_(arg.auditor)
, close_(arg.close)
{
if (arg.fund)
{
env_.fund(arg.xrp, issuer_);
for (auto const& it : holders_)
env_.fund(arg.xrpHolders, it.second);
if (arg.auditor)
env_.fund(arg.xrp, *arg.auditor);
}
if (close_)
env.close();
if (arg.fund)
{
env_.require(Owners(issuer_, 0));
for (auto const& it : holders_)
{
if (issuer_.id() == it.second.id())
Throw<std::runtime_error>("Issuer can't be holder");
env_.require(Owners(it.second, 0));
}
if (arg.auditor)
env_.require(Owners(*arg.auditor, 0));
}
if (arg.create)
create(*arg.create);
}
MPTTester::MPTTester(
Env& env,
Account issuer,
MPTID const& id,
std::vector<Account> const& holders,
bool close)
: env_(env), issuer_(std::move(issuer)), holders_(makeHolders(holders)), id_(id), close_(close)
{
}
static MPTCreate
makeMPTCreate(MPTInitDef const& arg)
{
if (arg.pay)
{
return {
.maxAmt = arg.maxAmt,
.transferFee = arg.transferFee,
.pay = {{arg.holders, *arg.pay}},
.flags = arg.flags,
.mutableFlags = arg.mutableFlags,
.authHolder = arg.authHolder};
}
return {
.maxAmt = arg.maxAmt,
.transferFee = arg.transferFee,
.authorize = arg.holders,
.flags = arg.flags,
.mutableFlags = arg.mutableFlags,
.authHolder = arg.authHolder};
}
MPTTester::MPTTester(MPTInitDef const& arg)
: MPTTester{
arg.env,
arg.issuer,
MPTInit{
.auditor = arg.auditor,
.fund = arg.fund,
.close = arg.close,
.create = makeMPTCreate(arg),
}}
{
}
MPTTester::
operator MPT() const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
return MPT("", *id_);
}
json::Value
MPTTester::createJV(MPTCreate const& arg)
{
if (!arg.issuer)
Throw<std::runtime_error>("MPTTester::createJV: issuer is not set");
json::Value jv;
jv[sfAccount] = arg.issuer->human();
if (arg.assetScale)
jv[sfAssetScale] = *arg.assetScale;
if (arg.transferFee)
jv[sfTransferFee] = *arg.transferFee;
if (arg.metadata)
jv[sfMPTokenMetadata] = strHex(*arg.metadata);
if (arg.maxAmt)
jv[sfMaximumAmount] = std::to_string(*arg.maxAmt);
if (arg.domainID)
jv[sfDomainID] = to_string(*arg.domainID);
if (arg.mutableFlags)
jv[sfMutableFlags] = *arg.mutableFlags;
jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
return jv;
}
void
MPTTester::create(MPTCreate const& arg)
{
if (id_)
Throw<std::runtime_error>("MPT can't be reused");
id_ = makeMptID(env_.seq(issuer_), issuer_);
json::Value const jv = createJV(
{.issuer = issuer_,
.maxAmt = arg.maxAmt,
.assetScale = arg.assetScale,
.transferFee = arg.transferFee,
.metadata = arg.metadata,
.mutableFlags = arg.mutableFlags,
.domainID = arg.domainID});
if (!isTesSuccess(submit(arg, jv)))
{
// Verify issuance doesn't exist
env_.require(
RequireAny([&]() -> bool { return env_.le(keylet::mptIssuance(*id_)) == nullptr; }));
id_.reset();
}
else
{
env_.require(MptFlags(*this, arg.flags.value_or(0)));
auto authAndPay = [&](auto const& accts, auto const&& getAcct) {
for (auto const& it : accts)
{
authorize({.account = getAcct(it)});
if ((arg.flags.value_or(0) & tfMPTRequireAuth) && arg.authHolder)
authorize({.account = issuer_, .holder = getAcct(it)});
if (arg.pay && arg.pay->first.empty())
pay(issuer_, getAcct(it), arg.pay->second);
}
if (arg.pay)
{
for (auto const& p : arg.pay->first)
pay(issuer_, p, arg.pay->second);
}
};
if (arg.authorize)
{
if (arg.authorize->empty())
{
authAndPay(holders_, [](auto const& it) { return it.second; });
}
else
{
authAndPay(*arg.authorize, [](auto const& it) { return it; });
}
}
else if (arg.pay)
{
if (arg.pay->first.empty())
{
authAndPay(holders_, [](auto const& it) { return it.second; });
}
else
{
authAndPay(arg.pay->first, [](auto const& it) { return it; });
}
}
}
}
json::Value
MPTTester::destroyJV(MPTDestroy const& arg)
{
json::Value jv;
if (!arg.issuer || !arg.id)
Throw<std::runtime_error>("MPTTester::destroyJV: issuer/id is not set");
jv[sfAccount] = arg.issuer->human();
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
jv[sfTransactionType] = jss::MPTokenIssuanceDestroy;
return jv;
}
void
MPTTester::destroy(MPTDestroy const& arg)
{
if (!arg.id && !id_)
Throw<std::runtime_error>("MPT has not been created");
json::Value const jv =
destroyJV({.issuer = arg.issuer ? arg.issuer : issuer_, .id = arg.id ? arg.id : id_});
submit(arg, jv);
}
Account const&
MPTTester::holder(std::string const& holder) const
{
auto const& it = holders_.find(holder);
if (it == holders_.cend())
Throw<std::runtime_error>("Holder is not found");
return it->second;
}
json::Value
MPTTester::authorizeJV(MPTAuthorize const& arg)
{
json::Value jv;
if (!arg.account || !arg.id)
Throw<std::runtime_error>("MPTTester::authorizeJV: account/id is not set");
jv[sfAccount] = arg.account->human();
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
if (arg.holder)
jv[sfHolder] = arg.holder->human();
jv[sfTransactionType] = jss::MPTokenAuthorize;
return jv;
}
void
MPTTester::authorize(MPTAuthorize const& arg)
{
if (!arg.id && !id_)
Throw<std::runtime_error>("MPT has not been created");
json::Value const jv = authorizeJV({
.account = arg.account ? arg.account : issuer_,
.holder = arg.holder,
.id = arg.id ? arg.id : id_,
});
if (auto const result = submit(arg, jv); isTesSuccess(result))
{
// Issuer authorizes
if (!arg.account || *arg.account == issuer_)
{
auto const flags = getFlags(arg.holder);
// issuer un-authorizes the holder
if (arg.flags.value_or(0) == tfMPTUnauthorize)
{
env_.require(MptFlags(*this, flags, arg.holder));
// issuer authorizes the holder
}
else
{
env_.require(MptFlags(*this, flags | lsfMPTAuthorized, arg.holder));
}
}
// Holder authorizes
else if (arg.flags.value_or(0) != tfMPTUnauthorize)
{
auto const flags = getFlags(arg.account);
// holder creates a token
env_.require(MptFlags(*this, flags, arg.account));
env_.require(MptBalance(*this, *arg.account, 0));
}
else
{
// Verify that the MPToken doesn't exist.
forObject([&](SLEP const& sle) { return env_.test.BEAST_EXPECT(!sle); }, arg.account);
}
}
else if (
arg.account && *arg.account != issuer_ && arg.flags.value_or(0) != tfMPTUnauthorize && id_)
{
if (result == tecDUPLICATE)
{
// Verify that MPToken already exists
env_.require(RequireAny([&]() -> bool {
return env_.le(keylet::mptoken(*id_, arg.account->id())) != nullptr;
}));
}
else
{
// Verify MPToken doesn't exist if holder failed authorizing(unless
// it already exists)
env_.require(RequireAny([&]() -> bool {
return env_.le(keylet::mptoken(*id_, arg.account->id())) == nullptr;
}));
}
}
}
void
MPTTester::authorizeHolders(Holders const& holders)
{
for (auto const& holder : holders)
{
authorize({.account = holder});
}
}
json::Value
MPTTester::setJV(MPTSet const& arg)
{
json::Value jv;
if (!arg.account || !arg.id)
Throw<std::runtime_error>("MPTTester::setJV: account and/or id is not set");
jv[sfAccount] = arg.account->human();
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
if (arg.holder)
{
std::visit(
[&jv]<typename T>(T const& holder) {
if constexpr (std::is_same_v<T, Account>)
{
jv[sfHolder] = holder.human();
}
else if constexpr (std::is_same_v<T, AccountID>)
{
jv[sfHolder] = toBase58(holder);
}
},
*arg.holder);
}
if (arg.delegate)
jv[sfDelegate] = arg.delegate->human();
if (arg.domainID)
jv[sfDomainID] = to_string(*arg.domainID);
if (arg.mutableFlags)
jv[sfMutableFlags] = *arg.mutableFlags;
if (arg.transferFee)
jv[sfTransferFee] = *arg.transferFee;
if (arg.metadata)
jv[sfMPTokenMetadata] = strHex(*arg.metadata);
if (arg.issuerPubKey)
jv[sfIssuerEncryptionKey] = strHex(*arg.issuerPubKey);
if (arg.auditorPubKey)
jv[sfAuditorEncryptionKey] = strHex(*arg.auditorPubKey);
jv[sfTransactionType] = jss::MPTokenIssuanceSet;
return jv;
}
void
MPTTester::set(MPTSet const& arg)
{
if (!arg.id && !id_)
Throw<std::runtime_error>("MPT has not been created");
json::Value const jv = setJV(
{.account = arg.account ? arg.account : issuer_,
.holder = arg.holder,
.id = arg.id ? arg.id : id_,
.mutableFlags = arg.mutableFlags,
.transferFee = arg.transferFee,
.metadata = arg.metadata,
.delegate = arg.delegate,
.domainID = arg.domainID,
.issuerPubKey = arg.issuerPubKey,
.auditorPubKey = arg.auditorPubKey});
if (submit(arg, jv) == tesSUCCESS && ((arg.flags.value_or(0) != 0u) || arg.mutableFlags))
{
if (((arg.flags.value_or(0) != 0u) || arg.mutableFlags))
{
auto require = [&](std::optional<Account> const& holder, bool unchanged) {
auto flags = getFlags(holder);
if (!unchanged)
{
if (arg.flags)
{
if (*arg.flags & tfMPTLock)
{
flags |= lsfMPTLocked;
}
else if (*arg.flags & tfMPTUnlock)
{
flags &= ~lsfMPTLocked;
}
}
if (arg.mutableFlags)
{
for (auto const& [setFlag, ledgerFlag] : mptSetFlagMappings)
{
if ((*arg.mutableFlags & setFlag) != 0u)
{
flags |= ledgerFlag;
}
}
if (*arg.mutableFlags & tmfMPTSetCanHoldConfidentialBalance)
flags |= tfMPTCanHoldConfidentialBalance;
}
}
env_.require(MptFlags(*this, flags, holder));
};
if (arg.account)
require(std::nullopt, arg.holder.has_value());
if (auto const account = (arg.holder ? std::get_if<Account>(&(*arg.holder)) : nullptr))
require(*account, false);
if (arg.issuerPubKey)
{
env_.require(RequireAny([&]() -> bool {
return forObject([&](SLEP const& sle) -> bool {
if (sle)
{
auto const issuerPubKey = getPubKey(issuer_);
if (!issuerPubKey)
{
Throw<std::runtime_error>(
"MPTTester::set: issuer's pubkey is not set");
}
return strHex((*sle)[sfIssuerEncryptionKey]) == strHex(*issuerPubKey);
}
return false;
});
}));
}
if (arg.auditorPubKey)
{
env_.require(RequireAny([&]() -> bool {
return forObject([&](SLEP const& sle) -> bool {
if (sle)
{
if (!auditor_.has_value())
Throw<std::runtime_error>("MPTTester::set: auditor is not set");
auto const auditorPubKey = getPubKey(*auditor_);
if (!auditorPubKey)
{
Throw<std::runtime_error>(
"MPTTester::set: auditor's pubkey is not set");
}
return strHex((*sle)[sfAuditorEncryptionKey]) == strHex(*auditorPubKey);
}
return false;
});
}));
}
}
}
}
bool
MPTTester::forObject(
std::function<bool(SLEP const& sle)> const& cb,
std::optional<Account> const& holder) const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
auto const key = holder ? keylet::mptoken(*id_, holder->id()) : keylet::mptIssuance(*id_);
if (auto const sle = env_.le(key))
return cb(sle);
return false;
}
[[nodiscard]] bool
MPTTester::checkDomainID(std::optional<uint256> expected) const
{
return forObject([&](SLEP const& sle) -> bool {
if (sle->isFieldPresent(sfDomainID))
return expected == sle->getFieldH256(sfDomainID);
return (!expected.has_value());
});
}
[[nodiscard]] bool
MPTTester::checkMPTokenAmount(Account const& holder, std::int64_t expectedAmount) const
{
return forObject(
[&](SLEP const& sle) { return expectedAmount == (*sle)[sfMPTAmount]; }, holder);
}
[[nodiscard]] bool
MPTTester::checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const
{
return forObject(
[&](SLEP const& sle) { return expectedAmount == (*sle)[sfOutstandingAmount]; });
}
[[nodiscard]] bool
MPTTester::checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const
{
return forObject([&](SLEP const& sle) {
return expectedAmount == (*sle)[~sfConfidentialOutstandingAmount].value_or(0);
});
}
[[nodiscard]] bool
MPTTester::checkFlags(uint32_t const expectedFlags, std::optional<Account> const& holder) const
{
return expectedFlags == getFlags(holder);
}
[[nodiscard]] bool
MPTTester::checkMetadata(std::string const& metadata) const
{
return forObject([&](SLEP const& sle) -> bool {
if (sle->isFieldPresent(sfMPTokenMetadata))
return strHex(sle->getFieldVL(sfMPTokenMetadata)) == strHex(metadata);
return false;
});
}
[[nodiscard]] bool
MPTTester::isMetadataPresent() const
{
return forObject(
[&](SLEP const& sle) -> bool { return sle->isFieldPresent(sfMPTokenMetadata); });
}
[[nodiscard]] bool
MPTTester::checkTransferFee(std::uint16_t transferFee) const
{
return forObject([&](SLEP const& sle) -> bool {
if (sle->isFieldPresent(sfTransferFee))
return sle->getFieldU16(sfTransferFee) == transferFee;
return false;
});
}
[[nodiscard]] bool
MPTTester::isTransferFeePresent() const
{
return forObject([&](SLEP const& sle) -> bool { return sle->isFieldPresent(sfTransferFee); });
}
void
MPTTester::pay(
Account const& src,
Account const& dest,
std::int64_t amount,
std::optional<TER> err,
std::optional<std::vector<std::string>> credentials)
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
auto const srcAmt = getBalance(src);
auto const destAmt = getBalance(dest);
auto const outstandingAmt = getBalance(issuer_);
if (credentials)
{
env_(
jtx::pay(src, dest, mpt(amount)),
Ter(err.value_or(tesSUCCESS)),
credentials::Ids(*credentials));
}
else
{
env_(jtx::pay(src, dest, mpt(amount)), Ter(err.value_or(tesSUCCESS)));
}
if (!isTesSuccess(env_.ter()))
amount = 0;
if (close_)
env_.close();
if (src == issuer_)
{
env_.require(MptBalance(*this, src, srcAmt + amount));
env_.require(MptBalance(*this, dest, destAmt + amount));
}
else if (dest == issuer_)
{
env_.require(MptBalance(*this, src, srcAmt - amount));
env_.require(MptBalance(*this, dest, destAmt - amount));
}
else
{
STAmount const saAmount = {*id_, amount};
auto const actual = multiply(saAmount, transferRate(*env_.current(), *id_)).mpt().value();
// Sender pays the transfer fee if any
env_.require(MptBalance(*this, src, srcAmt - actual));
env_.require(MptBalance(*this, dest, destAmt + amount));
// Outstanding amount is reduced by the transfer fee if any
env_.require(MptBalance(*this, issuer_, outstandingAmt - (actual - amount)));
}
}
void
MPTTester::claw(
Account const& issuer,
Account const& holder,
std::int64_t amount,
std::optional<TER> err)
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
auto const issuerAmt = getBalance(issuer);
auto const holderAmt = getBalance(holder);
env_(jtx::claw(issuer, mpt(amount), holder), Ter(err.value_or(tesSUCCESS)));
if (!isTesSuccess(env_.ter()))
amount = 0;
if (close_)
env_.close();
env_.require(MptBalance(*this, issuer, issuerAmt - std::min(holderAmt, amount)));
env_.require(MptBalance(*this, holder, holderAmt - std::min(holderAmt, amount)));
}
PrettyAmount
MPTTester::mpt(std::int64_t amount) const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
return xrpl::test::jtx::MPT(issuer_.name(), *id_)(amount);
}
MPTTester::
operator Asset() const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
return Asset(*id_);
}
std::int64_t
MPTTester::getBalance(Account const& account) const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
if (account == issuer_)
{
if (auto const sle = env_.le(keylet::mptIssuance(*id_)))
return sle->getFieldU64(sfOutstandingAmount);
}
else
{
if (auto const sle = env_.le(keylet::mptoken(*id_, account.id())))
return sle->getFieldU64(sfMPTAmount);
}
return 0;
}
std::int64_t
MPTTester::getIssuanceConfidentialBalance() const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
if (auto const sle = env_.le(keylet::mptIssuance(*id_)))
return (*sle)[~sfConfidentialOutstandingAmount].value_or(0);
return 0;
}
std::optional<Buffer>
MPTTester::getClawbackProof(
Account const& holder,
std::uint64_t amount,
Buffer const& privateKey,
uint256 const& contextHash) 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_));
if (!sleHolder || !sleIssuance)
return std::nullopt;
auto const ciphertextBlob = sleHolder->getFieldVL(sfIssuerEncryptedBalance);
if (ciphertextBlob.size() != kEcGamalEncryptedTotalLength)
return std::nullopt;
auto const pubKeyBlob = sleIssuance->getFieldVL(sfIssuerEncryptionKey);
if (pubKeyBlob.size() != kEcPubKeyLength)
return std::nullopt;
Buffer proof(kEcClawbackProofLength);
if (mpt_get_clawback_proof(
privateKey.data(),
pubKeyBlob.data(),
contextHash.data(),
amount,
ciphertextBlob.data(),
proof.data()) != 0)
{
return std::nullopt;
}
return proof;
}
std::optional<Buffer>
MPTTester::getSchnorrProof(Account const& account, uint256 const& ctxHash) const
{
auto const pubKey = getPubKey(account);
if (!pubKey || pubKey->size() != kEcPubKeyLength)
return std::nullopt;
auto const privKey = getPrivKey(account);
if (requireValue(privKey, "privKey").size() != kEcPrivKeyLength)
return std::nullopt;
Buffer proof(kEcSchnorrProofLength);
if (mpt_get_convert_proof(
requireValue(pubKey, "pubKey").data(),
requireValue(privKey, "privKey").data(),
ctxHash.data(),
proof.data()) != 0)
return std::nullopt;
return proof;
}
std::optional<Buffer>
MPTTester::getConfidentialSendProof(
Account const& sender,
std::uint64_t const amount,
std::vector<ConfidentialRecipient> const& recipients,
Slice const& blindingFactor,
uint256 const& contextHash,
PedersenProofParams const& amountParams,
PedersenProofParams const& balanceParams) const
{
auto const pedersenBalanceParams = makePedersenParams(balanceParams);
if (blindingFactor.size() != kEcBlindingFactorLength)
return std::nullopt;
auto const senderPrivKey = getPrivKey(sender);
if (!senderPrivKey)
return std::nullopt;
auto const senderPubKey = getPubKey(sender);
if (!senderPubKey || senderPubKey->size() != kEcPubKeyLength)
return std::nullopt;
if (amountParams.pedersenCommitment.size() != kEcPedersenCommitmentLength)
return std::nullopt;
// Build mpt_confidential_participant array
std::vector<mpt_confidential_participant> participants(recipients.size());
for (size_t i = 0; i < recipients.size(); ++i)
{
auto const& r = recipients[i];
if (r.encryptedAmount.size() != kEcGamalEncryptedTotalLength ||
r.publicKey.size() != kEcPubKeyLength)
{
return std::nullopt;
}
std::memcpy(participants[i].pubkey, r.publicKey.data(), kEcPubKeyLength);
std::memcpy(
participants[i].ciphertext, r.encryptedAmount.data(), kEcGamalEncryptedTotalLength);
}
size_t proofLen = kEcSendProofLength;
Buffer proof(proofLen);
if (mpt_get_confidential_send_proof(
senderPrivKey->data(),
senderPubKey->data(),
amount,
participants.data(),
recipients.size(),
blindingFactor.data(),
contextHash.data(),
amountParams.pedersenCommitment.data(),
&pedersenBalanceParams,
proof.data(),
&proofLen) != 0)
return std::nullopt;
return proof;
}
Buffer
MPTTester::getPedersenCommitment(std::uint64_t const amount, Buffer const& pedersenBlindingFactor)
{
// Blinding factor (rho) must be a 32-byte scalar
if (pedersenBlindingFactor.size() != kEcBlindingFactorLength)
Throw<std::runtime_error>("Invalid blinding factor size");
// secp256k1_mpt_pedersen_commit doesn't handle amount 0, return a trivial
// valid commitment for test purposes
if (amount == 0)
{
Buffer buf(kEcPedersenCommitmentLength);
std::memset(buf.data(), 0, kEcPedersenCommitmentLength);
buf.data()[0] = kEcCompressedPrefixEvenY;
buf.data()[kEcPedersenCommitmentLength - 1] = 0x01;
return buf;
}
Buffer buf(kEcPedersenCommitmentLength);
if (mpt_get_pedersen_commitment(amount, pedersenBlindingFactor.data(), buf.data()) != 0)
Throw<std::runtime_error>("Pedersen commitment generation failed");
return buf;
}
Buffer
MPTTester::getConvertBackProof(
Account const& holder,
std::uint64_t const amount,
uint256 const& contextHash,
PedersenProofParams const& pcParams) const
{
// Expected total proof length: compact sigma proof (128 bytes) + single bulletproof (688 bytes)
std::size_t constexpr kExpectedProofLength = kEcConvertBackProofLength;
auto const sleMptoken = env_.le(keylet::mptoken(issuanceID(), holder.id()));
if (!sleMptoken || !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
return gMakeZeroBuffer(kExpectedProofLength);
auto const holderPubKey = getPubKey(holder);
auto const holderPrivKey = getPrivKey(holder);
if (!holderPubKey || !holderPrivKey)
return gMakeZeroBuffer(kExpectedProofLength);
auto const pedersenParams = makePedersenParams(pcParams);
Buffer proof(kExpectedProofLength);
if (mpt_get_convert_back_proof(
holderPrivKey->data(),
holderPubKey->data(),
contextHash.data(),
amount,
&pedersenParams,
proof.data()) != 0)
return gMakeZeroBuffer(kExpectedProofLength);
return proof;
}
std::optional<Buffer>
MPTTester::getEncryptedBalance(Account const& account, EncryptedBalanceType option) const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
if (auto const sle = env_.le(keylet::mptoken(*id_, account.id())))
{
if (option == holderEncryptedInbox && sle->isFieldPresent(sfConfidentialBalanceInbox))
{
return Buffer(
(*sle)[sfConfidentialBalanceInbox].data(),
(*sle)[sfConfidentialBalanceInbox].size());
}
if (option == holderEncryptedSpending && sle->isFieldPresent(sfConfidentialBalanceSpending))
{
return Buffer(
(*sle)[sfConfidentialBalanceSpending].data(),
(*sle)[sfConfidentialBalanceSpending].size());
}
if (option == issuerEncryptedBalance && sle->isFieldPresent(sfIssuerEncryptedBalance))
{
return Buffer(
(*sle)[sfIssuerEncryptedBalance].data(), (*sle)[sfIssuerEncryptedBalance].size());
}
if (option == auditorEncryptedBalance && sle->isFieldPresent(sfAuditorEncryptedBalance))
{
return Buffer(
(*sle)[sfAuditorEncryptedBalance].data(), (*sle)[sfAuditorEncryptedBalance].size());
}
}
return {};
}
std::uint32_t
MPTTester::getFlags(std::optional<Account> const& holder) const
{
std::uint32_t flags = 0;
if (!forObject(
[&](SLEP const& sle) {
flags = sle->getFlags();
return true;
},
holder))
Throw<std::runtime_error>("Failed to get the flags");
return flags;
}
MPT
MPTTester::operator[](std::string const& name) const
{
return MPT(name, issuanceID());
}
PrettyAmount
MPTTester::operator()(std::int64_t amount) const
{
return MPT("", issuanceID())(amount);
}
template <typename T>
void
MPTTester::fillConversionCiphertexts(
T const& arg,
json::Value& jv,
Buffer& holderCiphertext,
Buffer& issuerCiphertext,
std::optional<Buffer>& auditorCiphertext,
Buffer& blindingFactor) const
{
blindingFactor = arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor();
// Handle Holder
if (arg.holderEncryptedAmt)
{
holderCiphertext = *arg.holderEncryptedAmt;
}
else
{
holderCiphertext = encryptAmount(
requireValue(arg.account, "account"), requireValue(arg.amt, "amt"), blindingFactor);
}
jv[sfHolderEncryptedAmount.jsonName] = strHex(holderCiphertext);
// Handle Issuer
if (arg.issuerEncryptedAmt)
{
issuerCiphertext = *arg.issuerEncryptedAmt;
}
else
{
issuerCiphertext = encryptAmount(issuer_, requireValue(arg.amt, "amt"), blindingFactor);
}
jv[sfIssuerEncryptedAmount.jsonName] = strHex(issuerCiphertext);
// Handle Auditor
if (arg.auditorEncryptedAmt)
{
auditorCiphertext = *arg.auditorEncryptedAmt;
}
else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false))
{
auditorCiphertext = encryptAmount(
requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor);
}
// Update auditor JSON only if ciphertext exists
if (auditorCiphertext)
jv[sfAuditorEncryptedAmount.jsonName] = strHex(*auditorCiphertext);
}
void
MPTTester::convert(MPTConvert 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::ConfidentialMPTConvert;
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.holderPubKey)
jv[sfHolderEncryptionKey.jsonName] = strHex(*arg.holderPubKey);
Buffer holderCiphertext;
Buffer issuerCiphertext;
std::optional<Buffer> auditorCiphertext;
Buffer blindingFactor;
fillConversionCiphertexts(
arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor);
jv[sfBlindingFactor.jsonName] = strHex(blindingFactor);
if (arg.proof)
{
jv[sfZKProof.jsonName] = *arg.proof;
}
else if (arg.fillSchnorrProof.value_or(arg.holderPubKey.has_value()))
{
// whether to automatically generate and attach a Schnorr proof:
// if fillSchnorrProof is explicitly set, follow its value;
// otherwise, default to generating the proof only if holder pub key is
// present.
auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account));
auto const contextHash =
getConvertContextHash(requireValue(arg.account, "account").id(), issuanceID(), seq);
auto const proof = getSchnorrProof(*arg.account, contextHash);
if (proof)
{
jv[sfZKProof.jsonName] = strHex(*proof);
}
else
{
jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSchnorrProofLength));
}
}
auto const holderAmt = getBalance(*arg.account);
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance)
Throw<std::runtime_error>("Failed to get Pre-convert balance");
std::optional<uint64_t> prevAuditorBalance;
if (arg.auditorEncryptedAmt || auditor_)
{
prevAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance);
if (!prevAuditorBalance)
Throw<std::runtime_error>("Failed to get Pre-convert balance");
}
auto const prevOutstanding = getIssuanceOutstandingBalance();
if (submit(arg, jv) == tesSUCCESS)
{
auto const postConfidentialOutstanding = getIssuanceConfidentialBalance();
auto const postOutstanding = getIssuanceOutstandingBalance();
env_.require(MptBalance(
*this, requireValue(arg.account, "account"), holderAmt - requireValue(arg.amt, "amt")));
env_.require(RequireAny([&]() -> bool {
return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding;
}));
env_.require(RequireAny([&]() -> bool {
return prevConfidentialOutstanding + *arg.amt == postConfidentialOutstanding;
}));
env_.require(RequireAny([&]() -> bool {
return getEncryptedBalance(*arg.account, holderEncryptedInbox).has_value();
}));
env_.require(RequireAny([&]() -> bool {
return getEncryptedBalance(*arg.account, holderEncryptedSpending).has_value();
}));
env_.require(RequireAny([&]() -> bool {
return getEncryptedBalance(*arg.account, issuerEncryptedBalance).has_value();
}));
auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance)
Throw<std::runtime_error>("Failed to get post-convert balance");
if (arg.auditorEncryptedAmt || auditor_)
{
auto const postAuditorBalance =
getDecryptedBalance(*arg.account, auditorEncryptedBalance);
if (!postAuditorBalance)
Throw<std::runtime_error>("Failed to get post-convert auditor balance");
env_.require(RequireAny([&]() -> bool {
return getEncryptedBalance(*arg.account, auditorEncryptedBalance).has_value();
}));
// auditor's encrypted balance is updated correctly
env_.require(RequireAny(
[&]() -> bool { return *prevAuditorBalance + *arg.amt == *postAuditorBalance; }));
}
// spending balance should not change
env_.require(
RequireAny([&]() -> bool { return *postSpendingBalance == *prevSpendingBalance; }));
// issuer's encrypted balance is updated correctly
env_.require(RequireAny(
[&]() -> bool { return *prevIssuerBalance + *arg.amt == *postIssuerBalance; }));
// holder's inbox balance is updated correctly
env_.require(RequireAny(
[&]() -> bool { return *prevInboxBalance + *arg.amt == *postInboxBalance; }));
// sum of holder's inbox and spending balance should equal to issuer's
// encrypted balance
env_.require(RequireAny([&]() -> bool {
return *postInboxBalance + *postSpendingBalance == *postIssuerBalance;
}));
if (arg.holderPubKey)
{
env_.require(RequireAny([&]() -> bool {
return forObject(
[&](SLEP const& sle) -> bool {
if (sle)
{
auto const holderPubKey = getPubKey(*arg.account);
if (!holderPubKey)
{
Throw<std::runtime_error>(
"MPTTester::convert: holder's pubkey is "
"not set");
}
return strHex((*sle)[sfHolderEncryptionKey]) == strHex(*holderPubKey);
}
return false;
},
arg.account);
}));
}
}
}
json::Value
MPTTester::convertJV(MPTConvert const& arg, std::uint32_t seq)
{
json::Value jv;
if (arg.account)
{
jv[sfAccount] = arg.account->human();
}
else
{
Throw<std::runtime_error>("Account not specified");
}
jv[jss::TransactionType] = jss::ConfidentialMPTConvert;
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.holderPubKey)
jv[sfHolderEncryptionKey.jsonName] = strHex(*arg.holderPubKey);
Buffer holderCiphertext;
Buffer issuerCiphertext;
std::optional<Buffer> auditorCiphertext;
Buffer blindingFactor;
fillConversionCiphertexts(
arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor);
jv[sfBlindingFactor.jsonName] = strHex(blindingFactor);
if (arg.proof)
{
jv[sfZKProof.jsonName] = *arg.proof;
}
else if (arg.fillSchnorrProof.value_or(arg.holderPubKey.has_value()))
{
auto const contextHash =
getConvertContextHash(requireValue(arg.account, "account").id(), issuanceID(), seq);
auto const proof = getSchnorrProof(*arg.account, contextHash);
if (proof)
{
jv[sfZKProof.jsonName] = strHex(*proof);
}
else
{
jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSchnorrProofLength));
}
}
return jv;
}
void
MPTTester::send(MPTConfidentialSend const& arg)
{
json::Value jv;
jv[jss::TransactionType] = jss::ConfidentialMPTSend;
if (arg.account)
{
jv[sfAccount] = arg.account->human();
}
else
{
Throw<std::runtime_error>("Account not specified");
}
if (arg.dest)
{
jv[sfDestination] = arg.dest->human();
}
else
{
Throw<std::runtime_error>("Destination not specified");
}
if (!arg.amt)
Throw<std::runtime_error>("Amount not specified for testing purposes");
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_);
}
Buffer const blindingFactor =
arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor();
// fill in the encrypted amounts if not provided
auto const senderAmt = arg.senderEncryptedAmt
? *arg.senderEncryptedAmt
: encryptAmount(*arg.account, *arg.amt, blindingFactor);
auto const destAmt = arg.destEncryptedAmt ? *arg.destEncryptedAmt
: encryptAmount(*arg.dest, *arg.amt, blindingFactor);
auto const issuerAmt = arg.issuerEncryptedAmt
? *arg.issuerEncryptedAmt
: encryptAmount(issuer_, *arg.amt, blindingFactor);
std::optional<Buffer> auditorAmt;
if (arg.auditorEncryptedAmt)
{
auditorAmt = arg.auditorEncryptedAmt;
}
else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false))
{
auditorAmt = encryptAmount(
requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor);
}
jv[sfSenderEncryptedAmount] = strHex(senderAmt);
jv[sfDestinationEncryptedAmount] = strHex(destAmt);
jv[sfIssuerEncryptedAmount] = strHex(issuerAmt);
if (auditorAmt)
jv[sfAuditorEncryptedAmount] = strHex(*auditorAmt);
if (arg.credentials)
{
auto& arr(jv[sfCredentialIDs.jsonName] = json::ValueType::Array);
for (auto const& hash : *arg.credentials)
arr.append(hash);
}
// Version counters before send
auto const prevSenderVersion = getMPTokenVersion(*arg.account);
auto const prevDestVersion = getMPTokenVersion(*arg.dest);
// Sender's previous confidential state
auto const prevSenderInbox = getDecryptedBalance(*arg.account, holderEncryptedInbox);
auto const prevSenderSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending);
auto const prevSenderIssuer = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
auto const prevSenderInboxEncrypted = getEncryptedBalance(*arg.account, holderEncryptedInbox);
auto const prevSenderSpendingEncrypted =
getEncryptedBalance(*arg.account, holderEncryptedSpending);
auto const prevSenderIssuerEncrypted =
getEncryptedBalance(*arg.account, issuerEncryptedBalance);
if (!prevSenderInbox || !prevSenderSpending || !prevSenderIssuer)
Throw<std::runtime_error>("Failed to get Pre-send balance");
std::optional<uint64_t> prevSenderAuditor;
auto const prevSenderAuditorEncrypted =
getEncryptedBalance(*arg.account, auditorEncryptedBalance);
if (arg.auditorEncryptedAmt || auditor_)
{
prevSenderAuditor = getDecryptedBalance(*arg.account, auditorEncryptedBalance);
if (!prevSenderAuditor)
Throw<std::runtime_error>("Failed to get Pre-send balance");
}
// Destination's previous confidential state
auto const prevDestInbox = getDecryptedBalance(*arg.dest, holderEncryptedInbox);
auto const prevDestSpending = getDecryptedBalance(*arg.dest, holderEncryptedSpending);
auto const prevDestIssuer = getDecryptedBalance(*arg.dest, issuerEncryptedBalance);
auto const prevDestInboxEncrypted = getEncryptedBalance(*arg.dest, holderEncryptedInbox);
auto const prevDestSpendingEncrypted = getEncryptedBalance(*arg.dest, holderEncryptedSpending);
auto const prevDestIssuerEncrypted = getEncryptedBalance(*arg.dest, issuerEncryptedBalance);
if (!prevDestInbox || !prevDestSpending || !prevDestIssuer)
Throw<std::runtime_error>("Failed to get Pre-send balance");
std::optional<uint64_t> prevDestAuditor;
auto const prevDestAuditorEncrypted = getEncryptedBalance(*arg.dest, auditorEncryptedBalance);
if (arg.auditorEncryptedAmt || auditor_)
{
prevDestAuditor = getDecryptedBalance(*arg.dest, auditorEncryptedBalance);
if (!prevDestAuditor)
Throw<std::runtime_error>("Failed to get Pre-send balance");
}
// Fill in the commitment if not provided
// The amount commitment must use the same blinding factor as the ElGamal
// encryption. The sigma proof links the two, so using different randomness
// for each would cause proof verification to fail.
Buffer amountCommitment, balanceCommitment;
if (arg.amountCommitment)
{
amountCommitment = *arg.amountCommitment;
}
else
{
amountCommitment = getPedersenCommitment(*arg.amt, blindingFactor);
}
jv[sfAmountCommitment] = strHex(amountCommitment);
auto const balanceBlindingFactor = generateBlindingFactor();
if (arg.balanceCommitment)
{
balanceCommitment = *arg.balanceCommitment;
}
else
{
balanceCommitment = getPedersenCommitment(*prevSenderSpending, balanceBlindingFactor);
}
jv[sfBalanceCommitment] = strHex(balanceCommitment);
// Fill in the proof if not provided
if (arg.proof)
{
jv[sfZKProof] = *arg.proof;
}
else
{
auto const version = getMPTokenVersion(*arg.account);
auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account));
auto const ctxHash = getSendContextHash(
requireValue(arg.account, "account").id(),
issuanceID(),
seq,
requireValue(arg.dest, "dest").id(),
version);
std::vector<ConfidentialRecipient> recipients;
auto const senderPubKey = getPubKey(*arg.account);
auto const destPubKey = getPubKey(*arg.dest);
auto const issuerPubKey = getPubKey(issuer_);
// If a key is missing, we skip adding the recipient. This intentionally
// causes proof generation to fail, triggering the dummy proof fallback.
if (senderPubKey)
{
recipients.push_back({
.publicKey = Slice(*senderPubKey),
.encryptedAmount = senderAmt,
});
}
if (destPubKey)
{
recipients.push_back({
.publicKey = Slice(*destPubKey),
.encryptedAmount = destAmt,
});
}
if (issuerPubKey)
{
recipients.push_back({
.publicKey = Slice(*issuerPubKey),
.encryptedAmount = issuerAmt,
});
}
std::optional<Buffer> auditorPubKey;
if (auditorAmt)
{
if (!auditor_)
Throw<std::runtime_error>("Auditor not registered");
auditorPubKey = getPubKey(*auditor_);
if (auditorPubKey)
{
recipients.push_back({
.publicKey = Slice(*auditorPubKey),
.encryptedAmount = *auditorAmt,
});
}
}
std::optional<Buffer> proof;
// Skip proof generation if encrypted balance is missing (e.g.,
// feature disabled), when the sender and destination are the same
// (malformed case causing pcm to be zero), or when spending balance
// is 0
if (arg.account != arg.dest && prevSenderSpendingEncrypted && *prevSenderSpending > 0)
{
proof = getConfidentialSendProof(
*arg.account,
*arg.amt,
recipients,
blindingFactor,
ctxHash,
{
.pedersenCommitment = amountCommitment,
.amt = *arg.amt,
.encryptedAmt = senderAmt,
.blindingFactor = blindingFactor,
},
{
.pedersenCommitment = balanceCommitment,
.amt = *prevSenderSpending,
.encryptedAmt = *prevSenderSpendingEncrypted,
.blindingFactor = balanceBlindingFactor,
});
}
if (proof)
{
jv[sfZKProof.jsonName] = strHex(*proof);
}
else
{
jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSendProofLength));
}
}
auto const senderPubAmt = getBalance(*arg.account);
auto const destPubAmt = getBalance(*arg.dest);
auto const prevCOA = getIssuanceConfidentialBalance();
auto const prevOA = getIssuanceOutstandingBalance();
if (submit(arg, jv) == tesSUCCESS)
{
auto const postCOA = getIssuanceConfidentialBalance();
auto const postOA = getIssuanceOutstandingBalance();
// Sender's post confidential state
auto const postSenderInbox = getDecryptedBalance(*arg.account, holderEncryptedInbox);
auto const postSenderSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending);
auto const postSenderIssuer = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
if (!postSenderInbox || !postSenderSpending || !postSenderIssuer)
Throw<std::runtime_error>("Failed to get Post-send balance");
// Destination's post confidential state
auto const postDestInbox = getDecryptedBalance(*arg.dest, holderEncryptedInbox);
auto const postDestSpending = getDecryptedBalance(*arg.dest, holderEncryptedSpending);
auto const postDestIssuer = getDecryptedBalance(*arg.dest, issuerEncryptedBalance);
if (!postDestInbox || !postDestSpending || !postDestIssuer)
Throw<std::runtime_error>("Failed to get Post-send balance");
// Public balances unchanged
env_.require(MptBalance(*this, *arg.account, senderPubAmt));
env_.require(MptBalance(*this, *arg.dest, destPubAmt));
// OA and COA unchanged
env_.require(RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; }));
env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; }));
// Verify sender changes
env_.require(RequireAny([&]() -> bool {
return *prevSenderSpending >= *arg.amt &&
*postSenderSpending == *prevSenderSpending - *arg.amt;
}));
env_.require(RequireAny([&]() -> bool { return postSenderInbox == prevSenderInbox; }));
env_.require(RequireAny([&]() -> bool {
return *prevSenderIssuer >= *arg.amt &&
*postSenderIssuer == *prevSenderIssuer - *arg.amt;
}));
// Verify destination changes
env_.require(
RequireAny([&]() -> bool { return *postDestInbox == *prevDestInbox + *arg.amt; }));
env_.require(RequireAny([&]() -> bool { return *postDestSpending == *prevDestSpending; }));
env_.require(
RequireAny([&]() -> bool { return *postDestIssuer == *prevDestIssuer + *arg.amt; }));
// Cross checks
env_.require(RequireAny(
[&]() -> bool { return *postSenderInbox + *postSenderSpending == *postSenderIssuer; }));
env_.require(RequireAny(
[&]() -> bool { return *postDestInbox + *postDestSpending == *postDestIssuer; }));
// Version: sender increments by 1; receiver version is unchanged by incoming sends
env_.require(RequireAny(
[&]() -> bool { return getMPTokenVersion(*arg.account) == prevSenderVersion + 1; }));
env_.require(
RequireAny([&]() -> bool { return getMPTokenVersion(*arg.dest) == prevDestVersion; }));
if (arg.auditorEncryptedAmt || auditor_)
{
auto const postSenderAuditor =
getDecryptedBalance(*arg.account, auditorEncryptedBalance);
auto const postDestAuditor = getDecryptedBalance(*arg.dest, auditorEncryptedBalance);
if (!postSenderAuditor || !postDestAuditor)
Throw<std::runtime_error>("Failed to get Post-send balance");
env_.require(RequireAny([&]() -> bool {
return *postSenderAuditor == *postSenderIssuer &&
*postDestAuditor == *postDestIssuer;
}));
// verify sender
env_.require(RequireAny([&]() -> bool {
return prevSenderAuditor >= *arg.amt &&
*postSenderAuditor == *prevSenderAuditor - *arg.amt;
}));
// verify dest
env_.require(RequireAny(
[&]() -> bool { return *postDestAuditor == *prevDestAuditor + *arg.amt; }));
}
}
}
json::Value
MPTTester::sendJV(
MPTConfidentialSend const& arg,
std::uint32_t seq,
std::optional<ConfidentialSendChainState> chain)
{
json::Value jv;
jv[jss::TransactionType] = jss::ConfidentialMPTSend;
if (arg.account)
{
jv[sfAccount] = arg.account->human();
}
else
{
Throw<std::runtime_error>("Account not specified");
}
if (arg.dest)
{
jv[sfDestination] = arg.dest->human();
}
else
{
Throw<std::runtime_error>("Destination not specified");
}
if (!arg.amt)
Throw<std::runtime_error>("Amount not specified for testing purposes");
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_);
}
Buffer const blindingFactor =
arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor();
auto const senderAmt = arg.senderEncryptedAmt
? *arg.senderEncryptedAmt
: encryptAmount(*arg.account, *arg.amt, blindingFactor);
auto const destAmt = arg.destEncryptedAmt ? *arg.destEncryptedAmt
: encryptAmount(*arg.dest, *arg.amt, blindingFactor);
auto const issuerAmt = arg.issuerEncryptedAmt
? *arg.issuerEncryptedAmt
: encryptAmount(issuer_, *arg.amt, blindingFactor);
std::optional<Buffer> auditorAmt;
if (arg.auditorEncryptedAmt)
{
auditorAmt = arg.auditorEncryptedAmt;
}
else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false))
{
auditorAmt = encryptAmount(
requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor);
}
jv[sfSenderEncryptedAmount] = strHex(senderAmt);
jv[sfDestinationEncryptedAmount] = strHex(destAmt);
jv[sfIssuerEncryptedAmount] = strHex(issuerAmt);
if (auditorAmt)
jv[sfAuditorEncryptedAmount] = strHex(*auditorAmt);
if (arg.credentials)
{
auto& arr(jv[sfCredentialIDs.jsonName] = json::ValueType::Array);
for (auto const& hash : *arg.credentials)
arr.append(hash);
}
std::uint64_t prevSenderSpending = 0;
std::optional<Buffer> prevEncryptedSenderSpending;
std::uint32_t version = 0;
if (chain)
{
prevSenderSpending = chain->spending;
prevEncryptedSenderSpending = chain->encSpending;
version = chain->version;
}
else
{
auto const ledgerSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending);
if (!ledgerSpending)
Throw<std::runtime_error>("Failed to get sender spending balance");
prevSenderSpending = *ledgerSpending;
prevEncryptedSenderSpending = getEncryptedBalance(*arg.account, holderEncryptedSpending);
version = getMPTokenVersion(*arg.account);
}
// The amount commitment must use the same blinding factor as the tx ElGamal
// encryption blinding factor.
Buffer amountCommitment, balanceCommitment;
if (arg.amountCommitment)
{
amountCommitment = *arg.amountCommitment;
}
else
{
amountCommitment = getPedersenCommitment(*arg.amt, blindingFactor);
}
jv[sfAmountCommitment] = strHex(amountCommitment);
auto const balanceBlindingFactor = generateBlindingFactor();
if (arg.balanceCommitment)
{
balanceCommitment = *arg.balanceCommitment;
}
else
{
balanceCommitment = getPedersenCommitment(prevSenderSpending, balanceBlindingFactor);
}
jv[sfBalanceCommitment] = strHex(balanceCommitment);
if (arg.proof)
{
jv[sfZKProof.jsonName] = *arg.proof;
}
else
{
auto const ctxHash = getSendContextHash(
requireValue(arg.account, "account").id(),
issuanceID(),
seq,
requireValue(arg.dest, "dest").id(),
version);
std::vector<ConfidentialRecipient> recipients;
auto const senderPubKey = getPubKey(*arg.account);
auto const destPubKey = getPubKey(*arg.dest);
auto const issuerPubKey = getPubKey(issuer_);
if (senderPubKey)
{
recipients.push_back({
.publicKey = Slice(*senderPubKey),
.encryptedAmount = senderAmt,
});
}
if (destPubKey)
{
recipients.push_back({
.publicKey = Slice(*destPubKey),
.encryptedAmount = destAmt,
});
}
if (issuerPubKey)
{
recipients.push_back({
.publicKey = Slice(*issuerPubKey),
.encryptedAmount = issuerAmt,
});
}
std::optional<Buffer> auditorPubKey;
if (auditorAmt)
{
if (!auditor_)
Throw<std::runtime_error>("Auditor not registered");
auditorPubKey = getPubKey(*auditor_);
if (auditorPubKey)
{
recipients.push_back({
.publicKey = Slice(*auditorPubKey),
.encryptedAmount = *auditorAmt,
});
}
}
std::optional<Buffer> proof;
// Skip proof generation when spending balance is 0
if (arg.account != arg.dest && prevEncryptedSenderSpending && prevSenderSpending > 0)
{
proof = getConfidentialSendProof(
*arg.account,
*arg.amt,
recipients,
blindingFactor,
ctxHash,
{
.pedersenCommitment = amountCommitment,
.amt = *arg.amt,
.encryptedAmt = senderAmt,
.blindingFactor = blindingFactor,
},
{
.pedersenCommitment = balanceCommitment,
.amt = prevSenderSpending,
.encryptedAmt = *prevEncryptedSenderSpending,
.blindingFactor = balanceBlindingFactor,
});
}
if (proof)
{
jv[sfZKProof.jsonName] = strHex(*proof);
}
else
{
jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSendProofLength));
}
}
return jv;
}
static Buffer
parseSenderEncAmt(json::Value const& jv)
{
auto const hexStr = jv[sfSenderEncryptedAmount.jsonName].asString();
auto const bytes = strUnHex(hexStr);
if (!bytes)
Throw<std::runtime_error>("chainAfterSend: invalid hex in sfSenderEncryptedAmount");
return Buffer(bytes->data(), bytes->size());
}
ConfidentialSendChainState
MPTTester::chainAfterSend(Account const& sender, std::uint64_t sendAmt, json::Value const& jv) const
{
auto const prevSpending = getDecryptedBalance(sender, holderEncryptedSpending);
auto const prevEncSpending = getEncryptedBalance(sender, holderEncryptedSpending);
auto const prevVersion = getMPTokenVersion(sender);
if (!prevSpending || !prevEncSpending)
Throw<std::runtime_error>("chainAfterSend: failed to read sender state from ledger");
Buffer const senderEncAmt = parseSenderEncAmt(jv);
auto chain = computeNextSendChainState(
*prevSpending, Slice(*prevEncSpending), prevVersion, sendAmt, Slice(senderEncAmt));
if (!chain)
Throw<std::runtime_error>("chainAfterSend: computeNextSendChainState failed");
return std::move(*chain);
}
std::optional<ConfidentialSendChainState>
computeNextSendChainState(
std::uint64_t currentSpending,
Slice const& currentEncSpending,
std::uint32_t currentVersion,
std::uint64_t sendAmt,
Slice const& senderEncAmt)
{
if (sendAmt > currentSpending)
return std::nullopt; // LCOV_EXCL_LINE
auto newEncSpending = homomorphicSubtract(currentEncSpending, senderEncAmt);
if (!newEncSpending)
return std::nullopt; // LCOV_EXCL_LINE
return ConfidentialSendChainState{
.spending = currentSpending - sendAmt,
.encSpending = std::move(*newEncSpending),
.version = currentVersion + 1,
};
}
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::ConfidentialMPTClawback;
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;
}
else
{
auto const seq = arg.ticketSeq ? *arg.ticketSeq : env_.seq(account);
auto const contextHash = getClawbackContextHash(
account.id(), issuanceID(), seq, requireValue(arg.holder, "holder").id());
auto const privKey = getPrivKey(account);
if (!privKey || privKey->size() != kEcPrivKeyLength)
Throw<std::runtime_error>("Failed to get clawback private key");
auto const proof = getClawbackProof(
requireValue(arg.holder, "holder"),
requireValue(arg.amt, "amt"),
requireValue(privKey, "privKey"),
contextHash);
if (proof)
{
jv[sfZKProof] = strHex(*proof);
}
else
{
jv[sfZKProof] = strHex(gMakeZeroBuffer(kEcClawbackProofLength));
}
}
auto const holderPubAmt = getBalance(*arg.holder);
auto const prevCOA = getIssuanceConfidentialBalance();
auto const prevOA = getIssuanceOutstandingBalance();
auto const prevVersion = getMPTokenVersion(*arg.holder);
if (submit(arg, jv) == tesSUCCESS)
{
auto const postCOA = getIssuanceConfidentialBalance();
auto const postOA = getIssuanceOutstandingBalance();
auto const postVersion = getMPTokenVersion(*arg.holder);
// 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 && postOA && *prevOA >= *arg.amt && *postOA == *prevOA - *arg.amt;
}));
// Verify holder's confidential balances are zeroed out
env_.require(RequireAny(
[&]() -> bool { return getDecryptedBalance(*arg.holder, holderEncryptedInbox) == 0; }));
env_.require(RequireAny([&]() -> bool {
return getDecryptedBalance(*arg.holder, holderEncryptedSpending) == 0;
}));
env_.require(RequireAny([&]() -> bool {
return getDecryptedBalance(*arg.holder, issuerEncryptedBalance) == 0;
}));
env_.require(RequireAny([&]() -> bool {
return getDecryptedBalance(*arg.holder, auditorEncryptedBalance) == 0;
}));
// Verify version is incremented
env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }));
}
}
void
MPTTester::generateKeyPair(Account const& account)
{
unsigned char privKey[kEcPrivKeyLength];
secp256k1_pubkey pubKey;
if (secp256k1_elgamal_generate_keypair(secp256k1Context(), privKey, &pubKey) == 0)
Throw<std::runtime_error>("failed to generate key pair");
// Serialize public key to compressed format (33 bytes)
unsigned char compressedPubKey[kEcPubKeyLength];
size_t outLen = kEcPubKeyLength;
if (secp256k1_ec_pubkey_serialize(
secp256k1Context(), compressedPubKey, &outLen, &pubKey, SECP256K1_EC_COMPRESSED) != 1 ||
outLen != kEcPubKeyLength)
{
Throw<std::runtime_error>("failed to serialize public key");
}
pubKeys_.insert({account.id(), Buffer{compressedPubKey, kEcPubKeyLength}});
privKeys_.insert({account.id(), Buffer{privKey, kEcPrivKeyLength}});
}
std::optional<Buffer>
MPTTester::getPubKey(Account const& account) const
{
if (auto const it = pubKeys_.find(account.id()); it != pubKeys_.end())
return it->second;
return std::nullopt;
}
std::optional<Buffer>
MPTTester::getPrivKey(Account const& account) const
{
if (auto const it = privKeys_.find(account.id()); it != privKeys_.end())
return it->second;
return std::nullopt;
}
Buffer
MPTTester::encryptAmount(Account const& account, uint64_t const amt, Buffer const& blindingFactor)
const
{
if (auto const pubKey = getPubKey(account))
{
if (auto const result = xrpl::encryptAmount(amt, *pubKey, blindingFactor))
return *result;
}
// Return a dummy buffer on failure to allow testing of
// failures that occur prior to encryption.
return gMakeZeroBuffer(kEcGamalEncryptedTotalLength);
}
std::optional<uint64_t>
MPTTester::decryptAmount(Account const& account, Buffer const& amt) const
{
if (amt.size() != kEcGamalEncryptedTotalLength)
return std::nullopt;
auto const pair = makeEcPair(amt);
if (!pair)
return std::nullopt;
auto const privKey = getPrivKey(account);
if (!privKey || privKey->size() != kEcPrivKeyLength)
return std::nullopt;
uint64_t decryptedAmt = 0;
if (secp256k1_elgamal_decrypt(
secp256k1Context(),
&decryptedAmt,
&pair->c1,
&pair->c2,
privKey->data(),
kElGamalDecryptRangeLow,
kElGamalDecryptRangeHigh) == 0)
{
return std::nullopt;
}
return decryptedAmt;
}
std::optional<uint64_t>
MPTTester::getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const
{
auto const encryptedAmt = getEncryptedBalance(account, balanceType);
// Return zero to test cases like Feature Disabled, where the ledger object
// does not exist.
if (!encryptedAmt)
return 0;
Account decryptor = account;
if (balanceType == issuerEncryptedBalance)
{
decryptor = issuer_;
}
else if (balanceType == auditorEncryptedBalance)
{
if (!auditor_)
return std::nullopt;
decryptor = *auditor_;
}
return decryptAmount(decryptor, *encryptedAmt);
};
json::Value
MPTTester::mergeInboxJV(MPTMergeInbox const& arg) const
{
json::Value jv;
if (arg.account)
{
jv[sfAccount] = arg.account->human();
}
else
{
Throw<std::runtime_error>("Account not specified");
}
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_);
}
jv[sfTransactionType] = jss::ConfidentialMPTMergeInbox;
return jv;
}
void
MPTTester::mergeInbox(MPTMergeInbox const& arg)
{
json::Value jv;
if (arg.account)
{
jv[sfAccount] = arg.account->human();
}
else
{
Throw<std::runtime_error>("Account not specified");
}
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_);
}
jv[sfTransactionType] = jss::ConfidentialMPTMergeInbox;
auto const holderPubAmt = getBalance(*arg.account);
auto const prevCOA = getIssuanceConfidentialBalance();
auto const prevOA = getIssuanceOutstandingBalance();
auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
auto const prevIssuerEncrypted = getEncryptedBalance(*arg.account, issuerEncryptedBalance);
auto const prevAuditorEncrypted = getEncryptedBalance(*arg.account, auditorEncryptedBalance);
auto const prevVersion = getMPTokenVersion(*arg.account);
if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance)
Throw<std::runtime_error>("Failed to get pre-mergeInbox balances");
if (submit(arg, jv) == tesSUCCESS)
{
auto const postCOA = getIssuanceConfidentialBalance();
auto const postOA = getIssuanceOutstandingBalance();
auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
auto const postInboxEncrypted = getEncryptedBalance(*arg.account, holderEncryptedInbox);
auto const postIssuerEncrypted = getEncryptedBalance(*arg.account, issuerEncryptedBalance);
auto const postAuditorEncrypted =
getEncryptedBalance(*arg.account, auditorEncryptedBalance);
auto const postVersion = getMPTokenVersion(*arg.account);
if (!postInboxBalance || !postSpendingBalance || !postIssuerBalance ||
!prevIssuerEncrypted || !postInboxEncrypted || !postIssuerEncrypted)
Throw<std::runtime_error>("Failed to get post-mergeInbox balances");
env_.require(MptBalance(*this, *arg.account, holderPubAmt));
env_.require(RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; }));
env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; }));
env_.require(RequireAny([&]() -> bool {
return *postSpendingBalance == *prevInboxBalance + *prevSpendingBalance &&
*postInboxBalance == 0;
}));
env_.require(
RequireAny([&]() -> bool { return *prevIssuerBalance == *postIssuerBalance; }));
auto const holderPubKey = getPubKey(*arg.account);
if (!holderPubKey)
Throw<std::runtime_error>("Failed to get holder public key");
auto const expectedInbox = encryptCanonicalZeroAmount(
requireValue(holderPubKey, "holderPubKey"),
requireValue(arg.account, "account").id(),
issuanceID());
if (!expectedInbox)
Throw<std::runtime_error>("Failed to get canonical zero encryption");
env_.require(RequireAny([&]() -> bool { return *postInboxEncrypted == *expectedInbox; }));
env_.require(
RequireAny([&]() -> bool { return *postIssuerEncrypted == *prevIssuerEncrypted; }));
env_.require(RequireAny([&]() -> bool {
return postAuditorEncrypted.has_value() == prevAuditorEncrypted.has_value() &&
(!postAuditorEncrypted || *postAuditorEncrypted == *prevAuditorEncrypted);
}));
env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }));
env_.require(RequireAny([&]() -> bool {
return *postSpendingBalance + *postInboxBalance == *postIssuerBalance;
}));
}
}
std::optional<std::int64_t>
MPTTester::getIssuanceOutstandingBalance() const
{
if (!id_)
return std::nullopt;
auto const sle = env_.current()->read(keylet::mptIssuance(*id_));
if (!sle)
return std::nullopt;
return (*sle)[sfOutstandingAmount];
}
std::uint32_t
MPTTester::getMPTokenVersion(Account const account) const
{
if (!id_)
Throw<std::runtime_error>("Issuance ID does not exist");
auto const sle = env_.current()->read(keylet::mptoken(*id_, account));
// return 0 here instead of throwing an exception since tests for
// preclaim will check if the MPToken exists
if (!sle)
return 0;
return (*sle)[~sfConfidentialBalanceVersion].value_or(0);
}
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::ConfidentialMPTConvertBack;
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);
Buffer holderCiphertext;
Buffer issuerCiphertext;
std::optional<Buffer> auditorCiphertext;
Buffer blindingFactor;
fillConversionCiphertexts(
arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor);
jv[sfBlindingFactor] = strHex(blindingFactor);
auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance)
Throw<std::runtime_error>("Failed to get Pre-convertBack balance");
Buffer pedersenCommitment;
Buffer const pcBlindingFactor = generateBlindingFactor();
if (arg.pedersenCommitment)
{
pedersenCommitment = *arg.pedersenCommitment;
}
else
{
pedersenCommitment = getPedersenCommitment(*prevSpendingBalance, pcBlindingFactor);
}
jv[sfBalanceCommitment] = strHex(pedersenCommitment);
if (arg.proof)
{
jv[sfZKProof.jsonName] = strHex(*arg.proof);
}
else
{
auto const version = getMPTokenVersion(*arg.account);
// if the caller generated ciphertexts themselves, they should also
// generate the proof themselves from the blinding factor
auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account));
auto const contextHash = getConvertBackContextHash(
requireValue(arg.account, "account").id(), issuanceID(), seq, version);
auto const prevEncryptedSpendingBalance =
getEncryptedBalance(*arg.account, holderEncryptedSpending);
Buffer proof;
// generate a dummy proof if no encrypted amount field, so that other
// preflight/preclaim are checked
if (!prevEncryptedSpendingBalance)
{
proof = gMakeZeroBuffer(kEcConvertBackProofLength);
}
else
{
proof = getConvertBackProof(
*arg.account,
requireValue(arg.amt, "amt"),
contextHash,
{
.pedersenCommitment = pedersenCommitment,
.amt = *prevSpendingBalance,
.encryptedAmt = *prevEncryptedSpendingBalance,
.blindingFactor = pcBlindingFactor,
});
}
jv[sfZKProof] = strHex(proof);
}
auto const holderAmt = getBalance(*arg.account);
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
std::optional<uint64_t> prevAuditorBalance;
if (arg.auditorEncryptedAmt || auditor_)
{
prevAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance);
if (!prevAuditorBalance)
Throw<std::runtime_error>("Failed to get Pre-convertBack balance");
}
auto const prevOutstanding = getIssuanceOutstandingBalance();
auto const prevVersion = getMPTokenVersion(*arg.account);
if (submit(arg, jv) == tesSUCCESS)
{
auto const postConfidentialOutstanding = getIssuanceConfidentialBalance();
auto const postOutstanding = getIssuanceOutstandingBalance();
auto const postVersion = getMPTokenVersion(*arg.account);
env_.require(MptBalance(
*this, requireValue(arg.account, "account"), holderAmt + requireValue(arg.amt, "amt")));
env_.require(RequireAny([&]() -> bool {
return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding;
}));
env_.require(RequireAny([&]() -> bool {
return prevConfidentialOutstanding - *arg.amt == postConfidentialOutstanding;
}));
auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox);
auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance);
auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance)
Throw<std::runtime_error>("Failed to get post-convertBack balance");
if (arg.auditorEncryptedAmt || auditor_)
{
auto const postAuditorBalance =
getDecryptedBalance(*arg.account, auditorEncryptedBalance);
if (!postAuditorBalance)
Throw<std::runtime_error>("Failed to get post-convertBack balance");
// auditor's encrypted balance is updated correctly
env_.require(RequireAny(
[&]() -> bool { return *prevAuditorBalance - *arg.amt == *postAuditorBalance; }));
}
// 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; }));
// holder's confidential balance version is updated correctly
env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; }));
// sum of holder's inbox and spending balance should equal to issuer's
// encrypted balance
env_.require(RequireAny([&]() -> bool {
return *postInboxBalance + *postSpendingBalance == *postIssuerBalance;
}));
}
}
json::Value
MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq)
{
json::Value jv;
if (arg.account)
{
jv[sfAccount] = arg.account->human();
}
else
{
Throw<std::runtime_error>("Account not specified");
}
jv[jss::TransactionType] = jss::ConfidentialMPTConvertBack;
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);
Buffer holderCiphertext;
Buffer issuerCiphertext;
std::optional<Buffer> auditorCiphertext;
Buffer blindingFactor;
fillConversionCiphertexts(
arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor);
jv[sfBlindingFactor] = strHex(blindingFactor);
auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending);
if (!prevSpendingBalance)
Throw<std::runtime_error>("convertBackJV: failed to read spending balance from ledger");
Buffer pedersenCommitment;
Buffer const pcBlindingFactor = generateBlindingFactor();
if (arg.pedersenCommitment)
{
pedersenCommitment = *arg.pedersenCommitment;
}
else
{
pedersenCommitment = getPedersenCommitment(*prevSpendingBalance, pcBlindingFactor);
}
jv[sfBalanceCommitment] = strHex(pedersenCommitment);
if (arg.proof)
{
jv[sfZKProof.jsonName] = strHex(*arg.proof);
}
else
{
auto const version = getMPTokenVersion(*arg.account);
auto const prevEncSpending = getEncryptedBalance(*arg.account, holderEncryptedSpending);
auto const contextHash = getConvertBackContextHash(
requireValue(arg.account, "account").id(), issuanceID(), seq, version);
Buffer proof;
if (!prevEncSpending)
{
proof = gMakeZeroBuffer(kEcConvertBackProofLength);
}
else
{
proof = getConvertBackProof(
*arg.account,
requireValue(arg.amt, "amt"),
contextHash,
{
.pedersenCommitment = pedersenCommitment,
.amt = *prevSpendingBalance,
.encryptedAmt = *prevEncSpending,
.blindingFactor = pcBlindingFactor,
});
}
jv[sfZKProof] = strHex(proof);
}
return jv;
}
} // namespace xrpl::test::jtx