mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-07 18:56:47 +00:00
1088 lines
34 KiB
C++
1088 lines
34 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2024 Ripple Labs Inc.
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <test/jtx.h>
|
|
|
|
#include <xrpl/protocol/ConfidentialTransfer.h>
|
|
#include <xrpl/protocol/SField.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
#include "test/jtx/mpt.h"
|
|
#include <openssl/rand.h>
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
|
|
namespace ripple {
|
|
namespace test {
|
|
namespace jtx {
|
|
|
|
ripple::Buffer
|
|
generatePlaceholderCiphertext()
|
|
{
|
|
Buffer buf(ecGamalEncryptedTotalLength);
|
|
|
|
buf.data()[0] = 0x02;
|
|
buf.data()[ecGamalEncryptedLength] = 0x02;
|
|
|
|
buf.data()[ecGamalEncryptedLength - 1] = 0x01;
|
|
buf.data()[ecGamalEncryptedTotalLength - 1] = 0x01;
|
|
|
|
return buf;
|
|
}
|
|
|
|
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))
|
|
, close_(arg.close)
|
|
{
|
|
if (arg.fund)
|
|
{
|
|
env_.fund(arg.xrp, issuer_);
|
|
for (auto it : holders_)
|
|
env_.fund(arg.xrpHolders, it.second);
|
|
}
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
jv[sfAccount] = issuer_.human();
|
|
jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
|
|
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;
|
|
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)));
|
|
}
|
|
|
|
void
|
|
MPTTester::destroy(MPTDestroy const& arg)
|
|
{
|
|
Json::Value jv;
|
|
if (arg.issuer)
|
|
jv[sfAccount] = arg.issuer->human();
|
|
else
|
|
jv[sfAccount] = issuer_.human();
|
|
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::MPTokenIssuanceDestroy;
|
|
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;
|
|
}
|
|
|
|
void
|
|
MPTTester::authorize(MPTAuthorize const& arg)
|
|
{
|
|
Json::Value jv;
|
|
if (arg.account)
|
|
jv[sfAccount] = arg.account->human();
|
|
else
|
|
jv[sfAccount] = issuer_.human();
|
|
jv[sfTransactionType] = jss::MPTokenAuthorize;
|
|
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.holder)
|
|
jv[sfHolder] = arg.holder->human();
|
|
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::set(MPTSet const& arg)
|
|
{
|
|
Json::Value jv;
|
|
if (arg.account)
|
|
jv[sfAccount] = arg.account->human();
|
|
else
|
|
jv[sfAccount] = issuer_.human();
|
|
jv[sfTransactionType] = jss::MPTokenIssuanceSet;
|
|
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.holder)
|
|
jv[sfHolder] = arg.holder->human();
|
|
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.pubKey)
|
|
jv[sfIssuerElGamalPublicKey] = strHex(*arg.pubKey);
|
|
if (submit(arg, jv) == tesSUCCESS)
|
|
{
|
|
if ((arg.flags || 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;
|
|
}
|
|
}
|
|
env_.require(mptflags(*this, flags, holder));
|
|
};
|
|
if (arg.account)
|
|
require(std::nullopt, arg.holder.has_value());
|
|
if (arg.holder)
|
|
require(*arg.holder, false);
|
|
}
|
|
|
|
if (arg.pubKey)
|
|
{
|
|
env_.require(requireAny([&]() -> bool {
|
|
return forObject([&](SLEP const& sle) -> bool {
|
|
if (sle)
|
|
{
|
|
return strHex((*sle)[sfIssuerElGamalPublicKey]) ==
|
|
strHex(getPubKey(issuer_));
|
|
}
|
|
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(); },
|
|
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 outstnAmt = 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_, outstnAmt - (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 ripple::test::jtx::MPT(issuer_.name(), *id_)(amount);
|
|
}
|
|
|
|
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::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());
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
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::ConfidentialConvert;
|
|
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);
|
|
|
|
if (arg.holderEncryptedAmt)
|
|
jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt);
|
|
else
|
|
jv[sfHolderEncryptedAmount.jsonName] =
|
|
strHex(encryptAmount(*arg.account, *arg.amt));
|
|
|
|
if (arg.issuerEncryptedAmt)
|
|
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
|
|
else
|
|
jv[sfIssuerEncryptedAmount.jsonName] =
|
|
strHex(encryptAmount(issuer_, *arg.amt));
|
|
|
|
if (arg.proof)
|
|
jv[sfZKProof.jsonName] = *arg.proof;
|
|
|
|
auto const holderAmt = getBalance(*arg.account);
|
|
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
|
|
|
|
uint64_t prevInboxBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
uint64_t prevSpendingBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
uint64_t prevIssuerBalance =
|
|
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
if (submit(arg, jv) == tesSUCCESS)
|
|
{
|
|
auto const postConfidentialOutstanding =
|
|
getIssuanceConfidentialBalance();
|
|
env_.require(mptbalance(*this, *arg.account, holderAmt - *arg.amt));
|
|
env_.require(requireAny([&]() -> bool {
|
|
return prevConfidentialOutstanding + *arg.amt ==
|
|
postConfidentialOutstanding;
|
|
}));
|
|
|
|
uint64_t postInboxBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
uint64_t postIssuerBalance =
|
|
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
uint64_t postSpendingBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
|
|
// std::cout << "\n postIssuerBalance is " << postIssuerBalance << '\n';
|
|
// std::cout << "\n postInboxBalance is " << postInboxBalance << '\n';
|
|
|
|
// spending balance should not change
|
|
env_.require(requireAny([&]() -> bool {
|
|
return postSpendingBalance == prevSpendingBalance;
|
|
}));
|
|
|
|
// 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)
|
|
{
|
|
return strHex((*sle)[sfHolderElGamalPublicKey]) ==
|
|
strHex(getPubKey(*arg.account));
|
|
}
|
|
return false;
|
|
},
|
|
*arg.account);
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MPTTester::send(MPTConfidentialSend const& arg)
|
|
{
|
|
Json::Value jv;
|
|
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");
|
|
|
|
jv[jss::TransactionType] = jss::ConfidentialSend;
|
|
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_);
|
|
}
|
|
|
|
// Generate the encrypted amounts if not provided
|
|
if (arg.senderEncryptedAmt)
|
|
jv[sfSenderEncryptedAmount.jsonName] = strHex(*arg.senderEncryptedAmt);
|
|
else
|
|
jv[sfSenderEncryptedAmount.jsonName] =
|
|
strHex(encryptAmount(*arg.account, *arg.amt));
|
|
|
|
if (arg.destEncryptedAmt)
|
|
jv[sfDestinationEncryptedAmount.jsonName] =
|
|
strHex(*arg.destEncryptedAmt);
|
|
else
|
|
jv[sfDestinationEncryptedAmount.jsonName] =
|
|
strHex(encryptAmount(*arg.dest, *arg.amt));
|
|
|
|
if (arg.issuerEncryptedAmt)
|
|
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
|
|
else
|
|
jv[sfIssuerEncryptedAmount.jsonName] =
|
|
strHex(encryptAmount(issuer_, *arg.amt));
|
|
|
|
if (arg.proof)
|
|
jv[sfZKProof.jsonName] = *arg.proof;
|
|
|
|
if (arg.credentials)
|
|
{
|
|
auto& arr(jv[sfCredentialIDs.jsonName] = Json::arrayValue);
|
|
for (auto const& hash : *arg.credentials)
|
|
arr.append(hash);
|
|
}
|
|
|
|
auto const senderPubAmt = getBalance(*arg.account);
|
|
auto const destPubAmt = getBalance(*arg.dest);
|
|
auto const prevCOA = getIssuanceConfidentialBalance();
|
|
auto const prevOA = getIssuanceOutstandingBalance();
|
|
|
|
// Sender's previous confidential state
|
|
uint64_t prevSenderInbox =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
uint64_t prevSenderSpending =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
uint64_t prevSenderIssuer =
|
|
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
// Destination's previous confidential state
|
|
uint64_t prevDestInbox =
|
|
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
|
|
uint64_t prevDestSpending =
|
|
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
|
|
uint64_t prevDestIssuer =
|
|
getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
if (submit(arg, jv) == tesSUCCESS)
|
|
{
|
|
auto const postCOA = getIssuanceConfidentialBalance();
|
|
auto const postOA = getIssuanceOutstandingBalance();
|
|
|
|
// Sender's post confidential state
|
|
uint64_t postSenderInbox =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
uint64_t postSenderSpending =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
uint64_t postSenderIssuer =
|
|
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
// Destination's post confidential state
|
|
uint64_t postDestInbox =
|
|
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
|
|
uint64_t postDestSpending =
|
|
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
|
|
uint64_t postDestIssuer =
|
|
getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_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;
|
|
}));
|
|
}
|
|
}
|
|
|
|
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}});
|
|
}
|
|
|
|
Buffer
|
|
MPTTester::getPubKey(Account const& account) const
|
|
{
|
|
auto it = pubKeys.find(account.id());
|
|
if (it != pubKeys.end())
|
|
{
|
|
return it->second;
|
|
}
|
|
|
|
Throw<std::runtime_error>("Account does not have public key");
|
|
}
|
|
|
|
Buffer
|
|
MPTTester::getPrivKey(Account const& account) const
|
|
{
|
|
auto it = privKeys.find(account.id());
|
|
if (it != privKeys.end())
|
|
{
|
|
return it->second;
|
|
}
|
|
|
|
Throw<std::runtime_error>("Account does not have private key");
|
|
}
|
|
|
|
Buffer
|
|
MPTTester::encryptAmount(Account const& account, uint64_t amt) const
|
|
{
|
|
return ripple::encryptAmount(amt, getPubKey(account));
|
|
}
|
|
|
|
uint64_t
|
|
MPTTester::decryptAmount(Account const& account, Buffer const& amt) const
|
|
{
|
|
secp256k1_pubkey c1;
|
|
secp256k1_pubkey c2;
|
|
|
|
uint64_t decryptedAmt;
|
|
|
|
if (!makeEcPair(amt, c1, c2))
|
|
Throw<std::runtime_error>(
|
|
"Failed to convert into individual EC components");
|
|
|
|
if (!secp256k1_elgamal_decrypt(
|
|
secp256k1Context(),
|
|
&decryptedAmt,
|
|
&c1,
|
|
&c2,
|
|
getPrivKey(account).data()))
|
|
Throw<std::runtime_error>("Failed to decrypt amount");
|
|
|
|
return decryptedAmt;
|
|
}
|
|
|
|
uint64_t
|
|
MPTTester::getDecryptedBalance(
|
|
Account const& account,
|
|
EncryptedBalanceType balanceType) const
|
|
|
|
{
|
|
auto maybeEncrypted = getEncryptedBalance(account, balanceType);
|
|
auto accountToDecrypt =
|
|
balanceType == ISSUER_ENCRYPTED_BALANCE ? issuer_ : account;
|
|
return maybeEncrypted ? decryptAmount(accountToDecrypt, *maybeEncrypted)
|
|
: 0;
|
|
};
|
|
|
|
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::ConfidentialMergeInbox;
|
|
uint64_t prevInboxBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
uint64_t prevSpendingBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
uint64_t prevIssuerBalance =
|
|
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
if (submit(arg, jv) == tesSUCCESS)
|
|
{
|
|
uint64_t postInboxBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
uint64_t postSpendingBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
uint64_t postIssuerBalance =
|
|
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
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];
|
|
}
|
|
|
|
void
|
|
MPTTester::convertBack(MPTConvertBack const& arg)
|
|
{
|
|
Json::Value jv;
|
|
if (arg.account)
|
|
jv[sfAccount] = arg.account->human();
|
|
else
|
|
Throw<std::runtime_error>("Account not specified");
|
|
|
|
jv[jss::TransactionType] = jss::ConfidentialConvertBack;
|
|
if (arg.id)
|
|
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
|
|
else
|
|
{
|
|
if (!id_)
|
|
Throw<std::runtime_error>("MPT has not been created");
|
|
jv[sfMPTokenIssuanceID] = to_string(*id_);
|
|
}
|
|
|
|
if (arg.amt)
|
|
jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
|
|
if (arg.holderEncryptedAmt)
|
|
jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt);
|
|
else
|
|
jv[sfHolderEncryptedAmount.jsonName] =
|
|
strHex(encryptAmount(*arg.account, *arg.amt));
|
|
|
|
if (arg.issuerEncryptedAmt)
|
|
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
|
|
else
|
|
jv[sfIssuerEncryptedAmount.jsonName] =
|
|
strHex(encryptAmount(issuer_, *arg.amt));
|
|
|
|
if (arg.proof)
|
|
jv[sfZKProof.jsonName] = *arg.proof;
|
|
|
|
auto const holderAmt = getBalance(*arg.account);
|
|
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
|
|
|
|
uint64_t prevInboxBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
uint64_t prevSpendingBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
uint64_t prevIssuerBalance =
|
|
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
|
|
if (submit(arg, jv) == tesSUCCESS)
|
|
{
|
|
auto const postConfidentialOutstanding =
|
|
getIssuanceConfidentialBalance();
|
|
env_.require(mptbalance(*this, *arg.account, holderAmt + *arg.amt));
|
|
env_.require(requireAny([&]() -> bool {
|
|
return prevConfidentialOutstanding - *arg.amt ==
|
|
postConfidentialOutstanding;
|
|
}));
|
|
|
|
uint64_t postInboxBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
|
uint64_t postIssuerBalance =
|
|
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
|
uint64_t postSpendingBalance =
|
|
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
|
|
|
// inbox balance should not change
|
|
env_.require(requireAny(
|
|
[&]() -> bool { return postInboxBalance == prevInboxBalance; }));
|
|
|
|
// issuer's encrypted balance is updated correctly
|
|
env_.require(requireAny([&]() -> bool {
|
|
return prevIssuerBalance - *arg.amt == postIssuerBalance;
|
|
}));
|
|
|
|
// holder's spending balance is updated correctly
|
|
env_.require(requireAny([&]() -> bool {
|
|
return prevSpendingBalance - *arg.amt == postSpendingBalance;
|
|
}));
|
|
|
|
// sum of holder's inbox and spending balance should equal to issuer's
|
|
// encrypted balance
|
|
env_.require(requireAny([&]() -> bool {
|
|
return postInboxBalance + postSpendingBalance == postIssuerBalance;
|
|
}));
|
|
}
|
|
}
|
|
|
|
} // namespace jtx
|
|
} // namespace test
|
|
} // namespace ripple
|