mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-07 10:47:05 +00:00
1831 lines
62 KiB
C++
1831 lines
62 KiB
C++
#include <test/jtx.h>
|
|
#include <test/jtx/mpt.h>
|
|
|
|
#include <xrpl/protocol/ConfidentialTransfer.h>
|
|
#include <xrpl/protocol/SField.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
#include <openssl/rand.h>
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
|
|
namespace xrpl {
|
|
namespace test {
|
|
namespace jtx {
|
|
|
|
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 const& issuer, MPTInit const& arg)
|
|
: env_(env), issuer_(issuer), holders_(makeHolders(arg.holders)), auditor_(arg.auditor), close_(arg.close)
|
|
{
|
|
if (arg.fund)
|
|
{
|
|
env_.fund(arg.xrp, issuer_);
|
|
for (auto 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 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 const& issuer, MPTID const& id, std::vector<Account> const& holders, bool close)
|
|
: env_(env), issuer_(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,
|
|
.authHolder = arg.authHolder};
|
|
return {
|
|
.maxAmt = arg.maxAmt,
|
|
.transferFee = arg.transferFee,
|
|
.authorize = arg.holders,
|
|
.flags = arg.flags,
|
|
.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 jv = createJV(
|
|
{.issuer = issuer_,
|
|
.maxAmt = arg.maxAmt,
|
|
.assetScale = arg.assetScale,
|
|
.transferFee = arg.transferFee,
|
|
.metadata = arg.metadata,
|
|
.mutableFlags = arg.mutableFlags,
|
|
.domainID = arg.domainID});
|
|
if (submit(arg, jv) != tesSUCCESS)
|
|
{
|
|
// 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 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 jv = authorizeJV({
|
|
.account = arg.account ? arg.account : issuer_,
|
|
.holder = arg.holder,
|
|
.id = arg.id ? arg.id : id_,
|
|
});
|
|
if (auto const result = submit(arg, jv); result == tesSUCCESS)
|
|
{
|
|
// 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[sfIssuerElGamalPublicKey] = strHex(*arg.issuerPubKey);
|
|
if (arg.auditorPubKey)
|
|
jv[sfAuditorElGamalPublicKey] = 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 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)
|
|
{
|
|
if ((arg.flags.value_or(0) || 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)
|
|
{
|
|
if (*arg.mutableFlags & tmfMPTSetCanLock)
|
|
flags |= lsfMPTCanLock;
|
|
else if (*arg.mutableFlags & tmfMPTClearCanLock)
|
|
flags &= ~lsfMPTCanLock;
|
|
|
|
if (*arg.mutableFlags & tmfMPTSetRequireAuth)
|
|
flags |= lsfMPTRequireAuth;
|
|
else if (*arg.mutableFlags & tmfMPTClearRequireAuth)
|
|
flags &= ~lsfMPTRequireAuth;
|
|
|
|
if (*arg.mutableFlags & tmfMPTSetCanEscrow)
|
|
flags |= lsfMPTCanEscrow;
|
|
else if (*arg.mutableFlags & tmfMPTClearCanEscrow)
|
|
flags &= ~lsfMPTCanEscrow;
|
|
|
|
if (*arg.mutableFlags & tmfMPTSetCanClawback)
|
|
flags |= lsfMPTCanClawback;
|
|
else if (*arg.mutableFlags & tmfMPTClearCanClawback)
|
|
flags &= ~lsfMPTCanClawback;
|
|
|
|
if (*arg.mutableFlags & tmfMPTSetCanTrade)
|
|
flags |= lsfMPTCanTrade;
|
|
else if (*arg.mutableFlags & tmfMPTClearCanTrade)
|
|
flags &= ~lsfMPTCanTrade;
|
|
|
|
if (*arg.mutableFlags & tmfMPTSetCanTransfer)
|
|
flags |= lsfMPTCanTransfer;
|
|
else if (*arg.mutableFlags & tmfMPTClearCanTransfer)
|
|
flags &= ~lsfMPTCanTransfer;
|
|
|
|
if (*arg.mutableFlags & tmfMPTSetPrivacy)
|
|
flags |= lsfMPTCanPrivacy;
|
|
else if (*arg.mutableFlags & tmfMPTClearPrivacy)
|
|
flags &= ~lsfMPTCanPrivacy;
|
|
}
|
|
}
|
|
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)[sfIssuerElGamalPublicKey]) == 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)[sfAuditorElGamalPublicKey]) == 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::printMPT(Account const& holder_) const
|
|
{
|
|
return forObject(
|
|
[&](SLEP const& sle) -> bool {
|
|
std::cout << "\n" << sle->getJson();
|
|
return true;
|
|
},
|
|
holder_);
|
|
}
|
|
|
|
[[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 (env_.ter() != tesSUCCESS)
|
|
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 (env_.ter() != tesSUCCESS)
|
|
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() != ecGamalEncryptedTotalLength)
|
|
return std::nullopt;
|
|
|
|
auto const pubKeyBlob = sleIssuance->getFieldVL(sfIssuerElGamalPublicKey);
|
|
if (pubKeyBlob.size() != ecPubKeyLength)
|
|
return std::nullopt;
|
|
|
|
secp256k1_pubkey c1, c2, pk;
|
|
auto const ctx = secp256k1Context();
|
|
|
|
if (!secp256k1_ec_pubkey_parse(ctx, &c1, ciphertextBlob.data(), ecGamalEncryptedLength))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (!secp256k1_ec_pubkey_parse(ctx, &c2, ciphertextBlob.data() + ecGamalEncryptedLength, ecGamalEncryptedLength))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::memcpy(pk.data, pubKeyBlob.data(), ecPubKeyLength);
|
|
Buffer proof(ecEqualityProofLength);
|
|
|
|
if (secp256k1_equality_plaintext_prove(
|
|
ctx, proof.data(), &pk, &c2, &c1, amount, privateKey.data(), contextHash.data()) != 1)
|
|
{
|
|
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() != ecPubKeyLength)
|
|
return std::nullopt;
|
|
|
|
auto const privKey = getPrivKey(account);
|
|
if (privKey->size() != ecPrivKeyLength)
|
|
return std::nullopt;
|
|
|
|
secp256k1_pubkey pk;
|
|
std::memcpy(pk.data, pubKey->data(), ecPubKeyLength);
|
|
|
|
Buffer proof(ecSchnorrProofLength);
|
|
|
|
if (secp256k1_mpt_pok_sk_prove(secp256k1Context(), proof.data(), &pk, privKey->data(), ctxHash.data()) != 1)
|
|
{
|
|
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,
|
|
std::size_t const nRecipients,
|
|
uint256 const& contextHash,
|
|
PedersenProofParams const& amountParams,
|
|
PedersenProofParams const& balanceParams) const
|
|
{
|
|
if (recipients.size() != nRecipients)
|
|
return std::nullopt;
|
|
|
|
if (blindingFactor.size() != ecBlindingFactorLength)
|
|
return std::nullopt;
|
|
|
|
auto const senderPubKey = getPubKey(sender);
|
|
if (!senderPubKey)
|
|
return std::nullopt;
|
|
|
|
auto const ctx = secp256k1Context();
|
|
|
|
std::vector<secp256k1_pubkey> r(nRecipients);
|
|
std::vector<secp256k1_pubkey> s(nRecipients);
|
|
std::vector<secp256k1_pubkey> pk(nRecipients);
|
|
|
|
std::vector<unsigned char> sr;
|
|
sr.reserve(nRecipients * ecBlindingFactorLength);
|
|
|
|
for (size_t i = 0; i < nRecipients; ++i)
|
|
{
|
|
auto const& recipient = recipients[i];
|
|
auto const* ctData = recipient.encryptedAmount.data();
|
|
|
|
if (recipient.encryptedAmount.size() != ecGamalEncryptedTotalLength)
|
|
return std::nullopt;
|
|
|
|
if (recipient.publicKey.size() != ecPubKeyLength)
|
|
return std::nullopt;
|
|
|
|
if (!secp256k1_ec_pubkey_parse(ctx, &r[i], ctData, ecGamalEncryptedLength))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (!secp256k1_ec_pubkey_parse(ctx, &s[i], ctData + ecGamalEncryptedLength, ecGamalEncryptedLength))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::memcpy(pk[i].data, recipient.publicKey.data(), ecPubKeyLength);
|
|
|
|
sr.insert(sr.end(), blindingFactor.data(), blindingFactor.data() + ecBlindingFactorLength);
|
|
}
|
|
|
|
size_t sizeEquality = secp256k1_mpt_prove_same_plaintext_multi_size(nRecipients);
|
|
Buffer equalityProof(sizeEquality);
|
|
|
|
// Get the multi-ciphertext equality proof
|
|
if (secp256k1_mpt_prove_same_plaintext_multi(
|
|
ctx,
|
|
equalityProof.data(),
|
|
&sizeEquality,
|
|
amount,
|
|
nRecipients,
|
|
r.data(),
|
|
s.data(),
|
|
pk.data(),
|
|
sr.data(),
|
|
contextHash.data()) != 1)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto const amountLinkageProof = getAmountLinkageProof(
|
|
*senderPubKey, Buffer(blindingFactor.data(), ecBlindingFactorLength), contextHash, amountParams);
|
|
|
|
auto const balanceLinkageProof = getBalanceLinkageProof(sender, contextHash, *senderPubKey, balanceParams);
|
|
|
|
auto const sizeAmountLinkage = amountLinkageProof.size();
|
|
auto const sizeBalanceLinkage = balanceLinkageProof.size();
|
|
|
|
size_t const proofSize = sizeEquality + sizeAmountLinkage + sizeBalanceLinkage;
|
|
Buffer proof(proofSize);
|
|
|
|
auto ptr = proof.data();
|
|
std::memcpy(ptr, equalityProof.data(), sizeEquality);
|
|
ptr += sizeEquality;
|
|
|
|
std::memcpy(ptr, amountLinkageProof.data(), sizeAmountLinkage);
|
|
ptr += sizeAmountLinkage;
|
|
|
|
std::memcpy(ptr, balanceLinkageProof.data(), sizeBalanceLinkage);
|
|
|
|
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() != ecBlindingFactorLength)
|
|
Throw<std::runtime_error>("Invalid blinding factor size");
|
|
|
|
// current pedersen generation implementation fails if amount is 0
|
|
if (amount == 0)
|
|
return Buffer{ecPedersenCommitmentLength};
|
|
|
|
secp256k1_pubkey commitment;
|
|
auto const ctx = secp256k1Context();
|
|
|
|
// Compute PC = m*G + rho*H
|
|
if (secp256k1_mpt_pedersen_commit(ctx, &commitment, amount, pedersenBlindingFactor.data()) != 1)
|
|
{
|
|
Throw<std::runtime_error>("Pedersen commitment generation failed");
|
|
}
|
|
|
|
return Buffer{commitment.data, ecPedersenCommitmentLength};
|
|
}
|
|
|
|
Buffer
|
|
MPTTester::getConvertBackProof(
|
|
Account const& holder,
|
|
std::uint64_t const amount,
|
|
uint256 const& contextHash,
|
|
Buffer const& holderCiphertext,
|
|
Buffer const& issuerCiphertext,
|
|
std::optional<Buffer> const& auditorCiphertext,
|
|
Buffer const& blindingFactor,
|
|
PedersenProofParams const& pcParams) const
|
|
{
|
|
auto const sleMptoken = env_.le(keylet::mptoken(*id_, holder.id()));
|
|
if (!sleMptoken || !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
|
|
return Buffer{};
|
|
|
|
auto const holderPubKey = getPubKey(holder);
|
|
if (holderPubKey)
|
|
{
|
|
Buffer const pedersenProof = getBalanceLinkageProof(holder, contextHash, *holderPubKey, pcParams);
|
|
|
|
// todo: incorporate range proof
|
|
return pedersenProof;
|
|
}
|
|
|
|
return Buffer{};
|
|
}
|
|
|
|
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 == HOLDER_ENCRYPTED_INBOX && sle->isFieldPresent(sfConfidentialBalanceInbox))
|
|
return Buffer((*sle)[sfConfidentialBalanceInbox].data(), (*sle)[sfConfidentialBalanceInbox].size());
|
|
if (option == HOLDER_ENCRYPTED_SPENDING && sle->isFieldPresent(sfConfidentialBalanceSpending))
|
|
return Buffer((*sle)[sfConfidentialBalanceSpending].data(), (*sle)[sfConfidentialBalanceSpending].size());
|
|
if (option == ISSUER_ENCRYPTED_BALANCE && sle->isFieldPresent(sfIssuerEncryptedBalance))
|
|
return Buffer((*sle)[sfIssuerEncryptedBalance].data(), (*sle)[sfIssuerEncryptedBalance].size());
|
|
if (option == AUDITOR_ENCRYPTED_BALANCE && 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(*arg.account, *arg.amt, blindingFactor);
|
|
|
|
jv[sfHolderEncryptedAmount.jsonName] = strHex(holderCiphertext);
|
|
|
|
// Handle Issuer
|
|
if (arg.issuerEncryptedAmt)
|
|
issuerCiphertext = *arg.issuerEncryptedAmt;
|
|
else
|
|
issuerCiphertext = encryptAmount(issuer_, *arg.amt, blindingFactor);
|
|
|
|
jv[sfIssuerEncryptedAmount.jsonName] = strHex(issuerCiphertext);
|
|
|
|
// Handle Auditor
|
|
if (arg.auditorEncryptedAmt)
|
|
auditorCiphertext = *arg.auditorEncryptedAmt;
|
|
else if (auditor_.has_value() && *arg.fillAuditorEncryptedAmt)
|
|
auditorCiphertext = encryptAmount(*auditor_, *arg.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[sfHolderElGamalPublicKey.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 contextHash = getConvertContextHash(arg.account->id(), env_.seq(*arg.account), *id_, *arg.amt);
|
|
|
|
auto const proof = getSchnorrProof(*arg.account, contextHash);
|
|
if (proof)
|
|
jv[sfZKProof.jsonName] = strHex(*proof);
|
|
else
|
|
jv[sfZKProof.jsonName] = strHex(Buffer(ecSchnorrProofLength));
|
|
}
|
|
|
|
auto const holderAmt = getBalance(*arg.account);
|
|
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
|
|
|
|
auto const prevInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
auto const prevSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
auto const prevIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
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, AUDITOR_ENCRYPTED_BALANCE);
|
|
if (!prevAuditorBalance)
|
|
Throw<std::runtime_error>("Failed to get Pre-convert 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; }));
|
|
|
|
auto const postInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
auto const postIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
auto const postSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
|
|
if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance)
|
|
Throw<std::runtime_error>("Failed to get post-convert balance");
|
|
|
|
if (arg.auditorEncryptedAmt || auditor_)
|
|
{
|
|
auto const postAuditorBalance = getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE);
|
|
|
|
if (!postAuditorBalance)
|
|
Throw<std::runtime_error>("Failed to get post-convert balance");
|
|
|
|
// 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)[sfHolderElGamalPublicKey]) == strHex(*holderPubKey);
|
|
}
|
|
return false;
|
|
},
|
|
*arg.account);
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
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())
|
|
auditorAmt = encryptAmount(*auditor_, *arg.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::arrayValue);
|
|
for (auto const& hash : *arg.credentials)
|
|
arr.append(hash);
|
|
}
|
|
|
|
// Sender's previous confidential state
|
|
auto const prevSenderInbox = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
auto const prevSenderSpending = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
auto const prevSenderIssuer = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
if (!prevSenderInbox || !prevSenderSpending || !prevSenderIssuer)
|
|
Throw<std::runtime_error>("Failed to get Pre-send balance");
|
|
|
|
std::optional<uint64_t> prevSenderAuditor;
|
|
if (arg.auditorEncryptedAmt || auditor_)
|
|
{
|
|
prevSenderAuditor = getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE);
|
|
if (!prevSenderAuditor)
|
|
Throw<std::runtime_error>("Failed to get Pre-send balance");
|
|
}
|
|
|
|
// Destination's previous confidential state
|
|
auto const prevDestInbox = getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
|
|
auto const prevDestSpending = getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
|
|
auto const prevDestIssuer = getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
|
|
if (!prevDestInbox || !prevDestSpending || !prevDestIssuer)
|
|
Throw<std::runtime_error>("Failed to get Pre-send balance");
|
|
|
|
std::optional<uint64_t> prevDestAuditor;
|
|
if (arg.auditorEncryptedAmt || auditor_)
|
|
{
|
|
prevDestAuditor = getDecryptedBalance(*arg.dest, AUDITOR_ENCRYPTED_BALANCE);
|
|
if (!prevDestAuditor)
|
|
Throw<std::runtime_error>("Failed to get Pre-send balance");
|
|
}
|
|
|
|
// Fill in the commitment if not provided
|
|
Buffer amountCommitment, balanceCommitment;
|
|
auto const amountBlindingFactor = generateBlindingFactor();
|
|
if (arg.amountCommitment)
|
|
amountCommitment = *arg.amountCommitment;
|
|
else
|
|
amountCommitment = getPedersenCommitment(*arg.amt, amountBlindingFactor);
|
|
|
|
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 ctxHash =
|
|
getSendContextHash(arg.account->id(), env_.seq(*arg.account), *id_, arg.dest->id(), version);
|
|
|
|
auto const nRecipients = getConfidentialRecipientCount(auditorAmt.has_value());
|
|
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 (due to recipient count mismatch),
|
|
// triggering the dummy proof fallback.
|
|
if (senderPubKey)
|
|
recipients.push_back({Slice(*senderPubKey), senderAmt});
|
|
if (destPubKey)
|
|
recipients.push_back({Slice(*destPubKey), destAmt});
|
|
if (issuerPubKey)
|
|
recipients.push_back({Slice(*issuerPubKey), issuerAmt});
|
|
|
|
std::optional<Buffer> auditorPubKey;
|
|
if (auditorAmt)
|
|
{
|
|
if (!auditor_)
|
|
Throw<std::runtime_error>("Auditor not registered");
|
|
|
|
auditorPubKey = getPubKey(*auditor_);
|
|
if (auditorPubKey)
|
|
recipients.push_back({Slice(*auditorPubKey), *auditorAmt});
|
|
}
|
|
|
|
auto const prevEncryptedSenderSpending = getEncryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
|
|
std::optional<Buffer> proof;
|
|
|
|
// Skip proof generation if encrypted balance is missing (e.g.,
|
|
// feature disabled), or when the sender and destination are the
|
|
// same (malformed case causing pcm to be zero). This prevents a
|
|
// crash and allows certain error cases to be tested.
|
|
if (arg.account != arg.dest && prevEncryptedSenderSpending)
|
|
{
|
|
proof = getConfidentialSendProof(
|
|
*arg.account,
|
|
*arg.amt,
|
|
recipients,
|
|
blindingFactor,
|
|
nRecipients,
|
|
ctxHash,
|
|
{.pedersenCommitment = amountCommitment,
|
|
.amt = *arg.amt,
|
|
.encryptedAmt = senderAmt,
|
|
.blindingFactor = amountBlindingFactor},
|
|
{.pedersenCommitment = balanceCommitment,
|
|
.amt = *prevSenderSpending,
|
|
.encryptedAmt = *prevEncryptedSenderSpending,
|
|
.blindingFactor = balanceBlindingFactor});
|
|
}
|
|
|
|
if (proof)
|
|
jv[sfZKProof.jsonName] = strHex(*proof);
|
|
else
|
|
{
|
|
size_t const dummySize = secp256k1_mpt_prove_same_plaintext_multi_size(nRecipients);
|
|
|
|
jv[sfZKProof.jsonName] = strHex(Buffer(dummySize));
|
|
}
|
|
}
|
|
|
|
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, HOLDER_ENCRYPTED_INBOX);
|
|
auto const postSenderSpending = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
auto const postSenderIssuer = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
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, HOLDER_ENCRYPTED_INBOX);
|
|
auto const postDestSpending = getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
|
|
auto const postDestIssuer = getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
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; }));
|
|
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; }));
|
|
|
|
if (arg.auditorEncryptedAmt || auditor_)
|
|
{
|
|
auto const postSenderAuditor = getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE);
|
|
auto const postDestAuditor = getDecryptedBalance(*arg.dest, AUDITOR_ENCRYPTED_BALANCE);
|
|
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; }));
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
{
|
|
std::uint32_t const seq = env_.seq(account);
|
|
uint256 const contextHash = getClawbackContextHash(account.id(), seq, *id_, *arg.amt, arg.holder->id());
|
|
|
|
auto const privKey = getPrivKey(account);
|
|
if (!privKey || privKey->size() != ecPrivKeyLength)
|
|
Throw<std::runtime_error>("Failed to get clawback private key");
|
|
|
|
auto const proof = getClawbackProof(*arg.holder, *arg.amt, *privKey, contextHash);
|
|
|
|
if (proof)
|
|
jv[sfZKProof] = strHex(*proof);
|
|
else
|
|
jv[sfZKProof] = strHex(Buffer(ecEqualityProofLength));
|
|
}
|
|
|
|
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; }));
|
|
env_.require(
|
|
requireAny([&]() -> bool { return getDecryptedBalance(*arg.holder, AUDITOR_ENCRYPTED_BALANCE) == 0; }));
|
|
}
|
|
}
|
|
|
|
void
|
|
MPTTester::generateKeyPair(Account const& account)
|
|
{
|
|
unsigned char privKey[ecPrivKeyLength];
|
|
secp256k1_pubkey pubKey;
|
|
if (!secp256k1_elgamal_generate_keypair(secp256k1Context(), privKey, &pubKey))
|
|
Throw<std::runtime_error>("failed to generate key pair");
|
|
|
|
pubKeys.insert({account.id(), Buffer{pubKey.data, ecPubKeyLength}});
|
|
privKeys.insert({account.id(), Buffer{privKey, ecPrivKeyLength}});
|
|
}
|
|
|
|
std::optional<Buffer>
|
|
MPTTester::getPubKey(Account const& account) const
|
|
{
|
|
auto it = pubKeys.find(account.id());
|
|
if (it != pubKeys.end())
|
|
{
|
|
return it->second;
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<Buffer>
|
|
MPTTester::getPrivKey(Account const& account) const
|
|
{
|
|
auto it = privKeys.find(account.id());
|
|
if (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 Buffer(ecGamalEncryptedTotalLength);
|
|
}
|
|
|
|
std::optional<uint64_t>
|
|
MPTTester::decryptAmount(Account const& account, Buffer const& amt) const
|
|
{
|
|
if (amt.size() != ecGamalEncryptedTotalLength)
|
|
return std::nullopt;
|
|
|
|
secp256k1_pubkey c1;
|
|
secp256k1_pubkey c2;
|
|
|
|
if (!makeEcPair(amt, c1, c2))
|
|
return std::nullopt;
|
|
|
|
auto const privKey = getPrivKey(account);
|
|
if (!privKey || privKey->size() != ecPrivKeyLength)
|
|
return std::nullopt;
|
|
|
|
uint64_t decryptedAmt;
|
|
if (!secp256k1_elgamal_decrypt(secp256k1Context(), &decryptedAmt, &c1, &c2, privKey->data()))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
return decryptedAmt;
|
|
}
|
|
|
|
std::optional<uint64_t>
|
|
MPTTester::getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const
|
|
|
|
{
|
|
auto 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 == ISSUER_ENCRYPTED_BALANCE)
|
|
decryptor = issuer_;
|
|
else if (balanceType == AUDITOR_ENCRYPTED_BALANCE)
|
|
{
|
|
if (!auditor_)
|
|
return std::nullopt;
|
|
decryptor = *auditor_;
|
|
}
|
|
|
|
return decryptAmount(decryptor, *encryptedAmt);
|
|
};
|
|
|
|
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 prevInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
auto const prevSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
auto const prevIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance)
|
|
Throw<std::runtime_error>("Failed to get pre-mergeInbox balances");
|
|
|
|
if (submit(arg, jv) == tesSUCCESS)
|
|
{
|
|
auto const postInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
auto const postSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
auto const postIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
if (!postInboxBalance || !postSpendingBalance || !postIssuerBalance)
|
|
Throw<std::runtime_error>("Failed to get post-mergeInbox balances");
|
|
|
|
env_.require(requireAny([&]() -> bool {
|
|
return *postSpendingBalance == *prevInboxBalance + *prevSpendingBalance && *postInboxBalance == 0;
|
|
}));
|
|
|
|
env_.require(requireAny([&]() -> bool { return *prevIssuerBalance == *postIssuerBalance; }));
|
|
|
|
env_.require(
|
|
requireAny([&]() -> bool { return *postSpendingBalance + *postInboxBalance == *postIssuerBalance; }));
|
|
}
|
|
}
|
|
|
|
std::int64_t
|
|
MPTTester::getIssuanceOutstandingBalance() const
|
|
{
|
|
if (!id_)
|
|
Throw<std::runtime_error>("Issuance ID does not exist");
|
|
|
|
auto const sle = env_.current()->read(keylet::mptIssuance(*id_));
|
|
|
|
if (!sle || !sle->isFieldPresent(sfOutstandingAmount))
|
|
Throw<std::runtime_error>("Issuance object does not contain outstanding amount");
|
|
|
|
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, HOLDER_ENCRYPTED_INBOX);
|
|
auto const prevSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
auto const prevIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance)
|
|
Throw<std::runtime_error>("Failed to get Pre-convertBack balance");
|
|
|
|
Buffer pedersenCommitment;
|
|
Buffer 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
|
|
uint256 const contextHash =
|
|
getConvertBackContextHash(arg.account->id(), env_.seq(*arg.account), *id_, *arg.amt, version);
|
|
auto const prevEncryptedSpendingBalance = getEncryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
|
|
Buffer proof;
|
|
// generate a dummy proof if no encrypted amount field, so that other
|
|
// preflight/preclaim are checked
|
|
if (!prevEncryptedSpendingBalance)
|
|
proof = Buffer();
|
|
else
|
|
{
|
|
proof = getConvertBackProof(
|
|
*arg.account,
|
|
*arg.amt,
|
|
contextHash,
|
|
holderCiphertext,
|
|
issuerCiphertext,
|
|
auditorCiphertext,
|
|
blindingFactor,
|
|
{
|
|
.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, AUDITOR_ENCRYPTED_BALANCE);
|
|
if (!prevAuditorBalance)
|
|
Throw<std::runtime_error>("Failed to get Pre-convertBack 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; }));
|
|
|
|
auto const postInboxBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
auto const postIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
auto const postSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
|
|
if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance)
|
|
Throw<std::runtime_error>("Failed to get post-convertBack balance");
|
|
|
|
if (arg.auditorEncryptedAmt || auditor_)
|
|
{
|
|
auto const postAuditorBalance = getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE);
|
|
|
|
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; }));
|
|
|
|
// sum of holder's inbox and spending balance should equal to issuer's
|
|
// encrypted balance
|
|
env_.require(
|
|
requireAny([&]() -> bool { return *postInboxBalance + *postSpendingBalance == *postIssuerBalance; }));
|
|
}
|
|
}
|
|
|
|
Buffer
|
|
MPTTester::getAmountLinkageProof(
|
|
Buffer const& pubKey,
|
|
Buffer const& blindingFactor,
|
|
uint256 const& contextHash,
|
|
PedersenProofParams const& params) const
|
|
{
|
|
if (params.blindingFactor.size() != ecBlindingFactorLength ||
|
|
params.pedersenCommitment.size() != ecPedersenCommitmentLength || pubKey.size() != ecPubKeyLength ||
|
|
params.encryptedAmt.size() != ecGamalEncryptedTotalLength || blindingFactor.size() != ecBlindingFactorLength)
|
|
return Buffer(ecPedersenProofLength);
|
|
|
|
secp256k1_pubkey c1, c2;
|
|
auto const ctx = secp256k1Context();
|
|
if (!secp256k1_ec_pubkey_parse(ctx, &c1, params.encryptedAmt.data(), ecGamalEncryptedLength) ||
|
|
!secp256k1_ec_pubkey_parse(
|
|
ctx, &c2, params.encryptedAmt.data() + ecGamalEncryptedLength, ecGamalEncryptedLength))
|
|
{
|
|
return Buffer();
|
|
}
|
|
|
|
secp256k1_pubkey pk;
|
|
std::memcpy(pk.data, pubKey.data(), ecPubKeyLength);
|
|
|
|
secp256k1_pubkey pcm;
|
|
std::memcpy(pcm.data, params.pedersenCommitment.data(), ecPedersenCommitmentLength);
|
|
|
|
Buffer proof(ecPedersenProofLength);
|
|
if (secp256k1_elgamal_pedersen_link_prove(
|
|
ctx,
|
|
proof.data(),
|
|
&c1,
|
|
&c2,
|
|
&pk,
|
|
&pcm,
|
|
params.amt,
|
|
blindingFactor.data(),
|
|
params.blindingFactor.data(),
|
|
contextHash.data()) != 1)
|
|
{
|
|
Throw<std::runtime_error>("Amount Linkage Proof generation failed");
|
|
}
|
|
|
|
return proof;
|
|
}
|
|
|
|
Buffer
|
|
MPTTester::getBalanceLinkageProof(
|
|
Account const& account,
|
|
uint256 const& contextHash,
|
|
Buffer const& pubKey,
|
|
PedersenProofParams const& params) const
|
|
{
|
|
if (params.blindingFactor.size() != ecBlindingFactorLength ||
|
|
params.pedersenCommitment.size() != ecPedersenCommitmentLength || pubKey.size() != ecPubKeyLength ||
|
|
params.encryptedAmt.size() != ecGamalEncryptedTotalLength)
|
|
return Buffer(ecPedersenProofLength);
|
|
|
|
secp256k1_pubkey c1, c2;
|
|
auto const ctx = secp256k1Context();
|
|
if (!secp256k1_ec_pubkey_parse(ctx, &c1, params.encryptedAmt.data(), ecGamalEncryptedLength) ||
|
|
!secp256k1_ec_pubkey_parse(
|
|
ctx, &c2, params.encryptedAmt.data() + ecGamalEncryptedLength, ecGamalEncryptedLength))
|
|
{
|
|
return Buffer();
|
|
}
|
|
|
|
secp256k1_pubkey pk;
|
|
std::memcpy(pk.data, pubKey.data(), ecPubKeyLength);
|
|
|
|
secp256k1_pubkey pcm;
|
|
std::memcpy(pcm.data, params.pedersenCommitment.data(), ecPedersenCommitmentLength);
|
|
|
|
Buffer proof(ecPedersenProofLength);
|
|
|
|
auto const privKey = getPrivKey(account);
|
|
if (!privKey || privKey->size() != ecPrivKeyLength)
|
|
Throw<std::runtime_error>("Failed to get Pedersen proof private key");
|
|
|
|
if (secp256k1_elgamal_pedersen_link_prove(
|
|
ctx,
|
|
proof.data(),
|
|
&pk,
|
|
&c2,
|
|
&c1,
|
|
&pcm,
|
|
params.amt,
|
|
privKey->data(),
|
|
params.blindingFactor.data(),
|
|
contextHash.data()) != 1)
|
|
Throw<std::runtime_error>("Pedersen proof generation failed");
|
|
|
|
return proof;
|
|
}
|
|
|
|
} // namespace jtx
|
|
} // namespace test
|
|
} // namespace xrpl
|