Files
rippled/src/test/app/XChain_test.cpp
Bart 1d42c4f6de refactor: Remove unnecessary copyright notices already covered by LICENSE.md (#5929)
Per XLS-0095, we are taking steps to rename ripple(d) to xrpl(d).

This change specifically removes all copyright notices referencing Ripple, XRPLF, and certain affiliated contributors upon mutual agreement, so the notice in the LICENSE.md file applies throughout. Copyright notices referencing external contributions remain as-is. Duplicate verbiage is also removed.
2025-11-04 08:33:42 +00:00

5178 lines
175 KiB
C++

#include <test/jtx.h>
#include <test/jtx/Env.h>
#include <test/jtx/attester.h>
#include <test/jtx/multisign.h>
#include <test/jtx/xchain_bridge.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XChainAttestations.h>
#include <functional>
#include <limits>
#include <optional>
#include <random>
#include <string>
#include <tuple>
#include <variant>
#include <vector>
namespace ripple::test {
// SEnv class - encapsulate jtx::Env to make it more user-friendly,
// for example having APIs that return a *this reference so that calls can be
// chained (fluent interface) allowing to create an environment and use it
// without encapsulating it in a curly brace block.
// ---------------------------------------------------------------------------
template <class T>
struct SEnv
{
jtx::Env env_;
SEnv(
T& s,
std::unique_ptr<Config> config,
FeatureBitset features,
std::unique_ptr<Logs> logs = nullptr,
beast::severities::Severity thresh = beast::severities::kError)
: env_(s, std::move(config), features, std::move(logs), thresh)
{
}
SEnv&
close()
{
env_.close();
return *this;
}
SEnv&
enableFeature(uint256 const feature)
{
env_.enableFeature(feature);
return *this;
}
SEnv&
disableFeature(uint256 const feature)
{
env_.app().config().features.erase(feature);
return *this;
}
template <class Arg, class... Args>
SEnv&
fund(STAmount const& amount, Arg const& arg, Args const&... args)
{
env_.fund(amount, arg, args...);
return *this;
}
template <class JsonValue, class... FN>
SEnv&
tx(JsonValue&& jv, FN const&... fN)
{
env_(std::forward<JsonValue>(jv), fN...);
return *this;
}
template <class... FN>
SEnv&
multiTx(jtx::JValueVec&& jvv, FN const&... fN)
{
for (auto const& jv : jvv)
env_(jv, fN...);
return *this;
}
TER
ter() const
{
return env_.ter();
}
STAmount
balance(jtx::Account const& account) const
{
return env_.balance(account).value();
}
STAmount
balance(jtx::Account const& account, Issue const& issue) const
{
return env_.balance(account, issue).value();
}
XRPAmount
reserve(std::uint32_t count)
{
return env_.current()->fees().accountReserve(count);
}
XRPAmount
txFee()
{
return env_.current()->fees().base;
}
std::shared_ptr<SLE const>
account(jtx::Account const& account)
{
return env_.le(account);
}
std::shared_ptr<SLE const>
bridge(Json::Value const& jvb)
{
STXChainBridge b(jvb);
auto tryGet =
[&](STXChainBridge::ChainType ct) -> std::shared_ptr<SLE const> {
if (auto r = env_.le(keylet::bridge(b, ct)))
{
if ((*r)[sfXChainBridge] == b)
return r;
}
return nullptr;
};
if (auto r = tryGet(STXChainBridge::ChainType::locking))
return r;
return tryGet(STXChainBridge::ChainType::issuing);
}
std::uint64_t
claimCount(Json::Value const& jvb)
{
return (*bridge(jvb))[sfXChainAccountClaimCount];
}
std::uint64_t
claimID(Json::Value const& jvb)
{
return (*bridge(jvb))[sfXChainClaimID];
}
std::shared_ptr<SLE const>
claimID(Json::Value const& jvb, std::uint64_t seq)
{
return env_.le(keylet::xChainClaimID(STXChainBridge(jvb), seq));
}
std::shared_ptr<SLE const>
caClaimID(Json::Value const& jvb, std::uint64_t seq)
{
return env_.le(
keylet::xChainCreateAccountClaimID(STXChainBridge(jvb), seq));
}
};
// XEnv class used for XChain tests. The only difference with SEnv<T> is that it
// funds some default accounts, and that it enables `testable_amendments() |
// FeatureBitset{featureXChainBridge}` by default.
// -----------------------------------------------------------------------------
template <class T>
struct XEnv : public jtx::XChainBridgeObjects, public SEnv<T>
{
XEnv(T& s, bool side = false) : SEnv<T>(s, jtx::envconfig(), features)
{
using namespace jtx;
STAmount xrp_funds{XRP(10000)};
if (!side)
{
this->fund(xrp_funds, mcDoor, mcAlice, mcBob, mcCarol, mcGw);
// Signer's list must match the attestation signers
// env_(jtx::signers(mcDoor, quorum, signers));
for (auto& s : signers)
this->fund(xrp_funds, s.account);
}
else
{
this->fund(
xrp_funds,
scDoor,
scAlice,
scBob,
scCarol,
scGw,
scAttester,
scReward);
for (auto& ra : payees)
this->fund(xrp_funds, ra);
for (auto& s : signers)
this->fund(xrp_funds, s.account);
// Signer's list must match the attestation signers
// env_(jtx::signers(Account::master, quorum, signers));
}
this->close();
}
};
// Tracks the xrp balance for one account
template <class T>
struct Balance
{
jtx::Account const& account_;
T& env_;
STAmount startAmount;
Balance(T& env, jtx::Account const& account) : account_(account), env_(env)
{
startAmount = env_.balance(account_);
}
STAmount
diff() const
{
return env_.balance(account_) - startAmount;
}
};
// Tracks the xrp balance for multiple accounts involved in a crosss-chain
// transfer
template <class T>
struct BalanceTransfer
{
using balance = Balance<T>;
balance from_;
balance to_;
balance payor_; // pays the rewards
std::vector<balance> reward_accounts; // receives the reward
XRPAmount txFees_;
BalanceTransfer(
T& env,
jtx::Account const& from_acct,
jtx::Account const& to_acct,
jtx::Account const& payor,
jtx::Account const* payees,
size_t num_payees,
bool withClaim)
: from_(env, from_acct)
, to_(env, to_acct)
, payor_(env, payor)
, reward_accounts([&]() {
std::vector<balance> r;
r.reserve(num_payees);
for (size_t i = 0; i < num_payees; ++i)
r.emplace_back(env, payees[i]);
return r;
}())
, txFees_(withClaim ? env.env_.current()->fees().base : XRPAmount(0))
{
}
BalanceTransfer(
T& env,
jtx::Account const& from_acct,
jtx::Account const& to_acct,
jtx::Account const& payor,
std::vector<jtx::Account> const& payees,
bool withClaim)
: BalanceTransfer(
env,
from_acct,
to_acct,
payor,
&payees[0],
payees.size(),
withClaim)
{
}
bool
payees_received(STAmount const& reward) const
{
return std::all_of(
reward_accounts.begin(),
reward_accounts.end(),
[&](balance const& b) { return b.diff() == reward; });
}
bool
check_most_balances(STAmount const& amt, STAmount const& reward)
{
return from_.diff() == -amt && to_.diff() == amt &&
payees_received(reward);
}
bool
has_happened(
STAmount const& amt,
STAmount const& reward,
bool check_payer = true)
{
auto reward_cost =
multiply(reward, STAmount(reward_accounts.size()), reward.issue());
return check_most_balances(amt, reward) &&
(!check_payer || payor_.diff() == -(reward_cost + txFees_));
}
bool
has_not_happened()
{
return check_most_balances(STAmount(0), STAmount(0)) &&
payor_.diff() <= txFees_; // could have paid fee for failed claim
}
};
struct BridgeDef
{
jtx::Account doorA;
Issue issueA;
jtx::Account doorB;
Issue issueB;
STAmount reward;
STAmount minAccountCreate;
uint32_t quorum;
std::vector<jtx::signer> const& signers;
Json::Value jvb;
template <class ENV>
void
initBridge(ENV& mcEnv, ENV& scEnv)
{
jvb = bridge(doorA, issueA, doorB, issueB);
auto const optAccountCreate = [&]() -> std::optional<STAmount> {
if (issueA != xrpIssue() || issueB != xrpIssue())
return {};
return minAccountCreate;
}();
mcEnv.tx(bridge_create(doorA, jvb, reward, optAccountCreate))
.tx(jtx::signers(doorA, quorum, signers))
.close();
scEnv.tx(bridge_create(doorB, jvb, reward, optAccountCreate))
.tx(jtx::signers(doorB, quorum, signers))
.close();
}
};
struct XChain_test : public beast::unit_test::suite,
public jtx::XChainBridgeObjects
{
XRPAmount
reserve(std::uint32_t count)
{
return XEnv(*this).env_.current()->fees().accountReserve(count);
}
XRPAmount
txFee()
{
return XEnv(*this).env_.current()->fees().base;
}
void
testXChainBridgeExtraFields()
{
auto jBridge = create_bridge(mcDoor)[sfXChainBridge.jsonName];
bool exceptionPresent = false;
try
{
exceptionPresent = false;
[[maybe_unused]] STXChainBridge testBridge1(jBridge);
}
catch (std::exception& ec)
{
exceptionPresent = true;
}
BEAST_EXPECT(!exceptionPresent);
try
{
exceptionPresent = false;
jBridge["Extra"] = 1;
[[maybe_unused]] STXChainBridge testBridge2(jBridge);
}
catch ([[maybe_unused]] std::exception& ec)
{
exceptionPresent = true;
}
BEAST_EXPECT(exceptionPresent);
}
void
testXChainCreateBridge()
{
XRPAmount res1 = reserve(1);
using namespace jtx;
testcase("Create Bridge");
// Normal create_bridge => should succeed
XEnv(*this).tx(create_bridge(mcDoor)).close();
// Bridge not owned by one of the door account.
XEnv(*this).tx(
create_bridge(mcBob), ter(temXCHAIN_BRIDGE_NONDOOR_OWNER));
// Create twice on the same account
XEnv(*this)
.tx(create_bridge(mcDoor))
.close()
.tx(create_bridge(mcDoor), ter(tecDUPLICATE));
// Create USD bridge Alice -> Bob ... should succeed
XEnv(*this).tx(
create_bridge(
mcAlice, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"])),
ter(tesSUCCESS));
// Create USD bridge, Alice is both the locking door and locking issue,
// ... should fail.
XEnv(*this).tx(
create_bridge(
mcAlice, bridge(mcAlice, mcAlice["USD"], mcBob, mcBob["USD"])),
ter(temXCHAIN_BRIDGE_BAD_ISSUES));
// Bridge where the two door accounts are equal.
XEnv(*this).tx(
create_bridge(
mcBob, bridge(mcBob, mcGw["USD"], mcBob, mcGw["USD"])),
ter(temXCHAIN_EQUAL_DOOR_ACCOUNTS));
// Both door accounts are on the same chain. This is not allowed.
// Although it doesn't violate any invariants, it's not a useful thing
// to do and it complicates the "add claim" transactions.
XEnv(*this)
.tx(create_bridge(
mcAlice, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"])))
.close()
.tx(create_bridge(
mcBob, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"])),
ter(tecDUPLICATE))
.close();
// Create a bridge on an account with exactly enough balance to
// meet the new reserve should succeed
XEnv(*this)
.fund(res1, mcuDoor) // exact reserve for account + 1 object
.close()
.tx(create_bridge(mcuDoor, jvub), ter(tesSUCCESS));
// Create a bridge on an account with no enough balance to meet the
// new reserve
XEnv(*this)
.fund(res1 - 1, mcuDoor) // just short of required reserve
.close()
.tx(create_bridge(mcuDoor, jvub), ter(tecINSUFFICIENT_RESERVE));
// Reward amount is non-xrp
XEnv(*this).tx(
create_bridge(mcDoor, jvb, mcUSD(1)),
ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT));
// Reward amount is XRP and negative
XEnv(*this).tx(
create_bridge(mcDoor, jvb, XRP(-1)),
ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT));
// Reward amount is 1 xrp => should succeed
XEnv(*this).tx(create_bridge(mcDoor, jvb, XRP(1)), ter(tesSUCCESS));
// Min create amount is 1 xrp, mincreate is 1 xrp => should succeed
XEnv(*this).tx(
create_bridge(mcDoor, jvb, XRP(1), XRP(1)), ter(tesSUCCESS));
// Min create amount is non-xrp
XEnv(*this).tx(
create_bridge(mcDoor, jvb, XRP(1), mcUSD(100)),
ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));
// Min create amount is zero (should fail, currently succeeds)
XEnv(*this).tx(
create_bridge(mcDoor, jvb, XRP(1), XRP(0)),
ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));
// Min create amount is negative
XEnv(*this).tx(
create_bridge(mcDoor, jvb, XRP(1), XRP(-1)),
ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));
// coverage test: BridgeCreate::preflight() - create bridge when feature
// disabled.
{
Env env(*this, testable_amendments() - featureXChainBridge);
env(create_bridge(Account::master, jvb), ter(temDISABLED));
}
// coverage test: BridgeCreate::preclaim() returns tecNO_ISSUER.
XEnv(*this).tx(
create_bridge(
mcAlice, bridge(mcAlice, mcuAlice["USD"], mcBob, mcBob["USD"])),
ter(tecNO_ISSUER));
// coverage test: create_bridge transaction with incorrect flag
XEnv(*this).tx(
create_bridge(mcAlice, jvb),
txflags(tfFillOrKill),
ter(temINVALID_FLAG));
// coverage test: create_bridge transaction with xchain feature disabled
XEnv(*this)
.disableFeature(featureXChainBridge)
.tx(create_bridge(mcAlice, jvb), ter(temDISABLED));
}
void
testXChainBridgeCreateConstraints()
{
/**
* Bridge create constraints tests.
*
* Define the door's bridge asset collection as the collection of all
* the issuing assets for which the door account is on the issuing chain
* and all the locking assets for which the door account is on the
* locking chain. (note: a door account can simultaneously be on an
* issuing and locking chain). A new bridge is not a duplicate as long
* as the new bridge asset collection does not contain any duplicate
* currencies (even if the issuers differ).
*
* Create bridges:
*
*| Owner | Locking | Issuing | Comment |
*| a1 | a1 USD/GW | USD/B | |
*| a2 | a2 USD/GW | USD/B | Same locking & issuing assets |
*| | | | |
*| a3 | a3 USD/GW | USD/a4 | |
*| a4 | a4 USD/GW | USD/a4 | Same bridge, different accounts |
*| | | | |
*| B | A USD/GW | USD/B | |
*| B | A EUR/GW | USD/B | Fail: Same issuing asset |
*| | | | |
*| A | A USD/B | USD/C | |
*| A | A USD/B | EUR/B | Fail: Same locking asset |
*| A | A USD/C | EUR/B | Fail: Same locking asset currency |
*| | | | |
*| A | A USD/GW | USD/B | Fail: Same bridge not allowed |
*| A | B USD/GW | USD/A | Fail: "A" has USD already |
*| B | A EUR/GW | USD/B | Fail: |
*
* Note that, now from sidechain's point of view, A is both
* a local locking door and a foreign locking door on different
* bridges. Txns such as commits specify bridge spec, but not the
* local door account. So we test the transactors can figure out
* the correct local door account from bridge spec.
*
* Commit to sidechain door accounts:
* | bridge spec | result
* case 6 | A -> B | B's balance increase
* case 7 | C <- A | A's balance increase
*
* We also test ModifyBridge txns modify correct bridges.
*/
using namespace jtx;
testcase("Bridge create constraints");
XEnv env(*this, true);
auto& A = scAlice;
auto& B = scBob;
auto& C = scCarol;
auto AUSD = A["USD"];
auto BUSD = B["USD"];
auto CUSD = C["USD"];
auto GUSD = scGw["USD"];
auto AEUR = A["EUR"];
auto BEUR = B["EUR"];
auto CEUR = C["EUR"];
auto GEUR = scGw["EUR"];
// Accounts to own single brdiges
Account const a1("a1");
Account const a2("a2");
Account const a3("a3");
Account const a4("a4");
Account const a5("a5");
Account const a6("a6");
env.fund(XRP(10000), a1, a2, a3, a4, a5, a6);
env.close();
// Add a bridge on two different accounts with the same locking and
// issuing assets
env.tx(create_bridge(a1, bridge(a1, GUSD, B, BUSD))).close();
env.tx(create_bridge(a2, bridge(a2, GUSD, B, BUSD))).close();
// Add the exact same bridge to two different accounts (one locking
// account and one issuing)
env.tx(create_bridge(a3, bridge(a3, GUSD, a4, a4["USD"]))).close();
env.tx(create_bridge(a4, bridge(a3, GUSD, a4, a4["USD"])),
ter(tecDUPLICATE))
.close();
// Add the exact same bridge to two different accounts (one issuing
// account and one locking - opposite order from the test above)
env.tx(create_bridge(a5, bridge(a6, GUSD, a5, a5["USD"]))).close();
env.tx(create_bridge(a6, bridge(a6, GUSD, a5, a5["USD"])),
ter(tecDUPLICATE))
.close();
// Test case 1 ~ 5, create bridges
auto const goodBridge1 = bridge(A, GUSD, B, BUSD);
auto const goodBridge2 = bridge(A, BUSD, C, CUSD);
env.tx(create_bridge(B, goodBridge1)).close();
// Issuing asset is the same, this is a duplicate
env.tx(create_bridge(B, bridge(A, GEUR, B, BUSD)), ter(tecDUPLICATE))
.close();
env.tx(create_bridge(A, goodBridge2), ter(tesSUCCESS)).close();
// Locking asset is the same - this is a duplicate
env.tx(create_bridge(A, bridge(A, BUSD, B, BEUR)), ter(tecDUPLICATE))
.close();
// Locking asset is USD - this is a duplicate even tho it has a
// different issuer
env.tx(create_bridge(A, bridge(A, CUSD, B, BEUR)), ter(tecDUPLICATE))
.close();
// Test case 6 and 7, commits
env.tx(trust(C, BUSD(1000)))
.tx(trust(A, BUSD(1000)))
.close()
.tx(pay(B, C, BUSD(1000)))
.close();
auto const aBalanceStart = env.balance(A, BUSD);
auto const cBalanceStart = env.balance(C, BUSD);
env.tx(xchain_commit(C, goodBridge1, 1, BUSD(50))).close();
BEAST_EXPECT(env.balance(A, BUSD) - aBalanceStart == BUSD(0));
BEAST_EXPECT(env.balance(C, BUSD) - cBalanceStart == BUSD(-50));
env.tx(xchain_commit(C, goodBridge2, 1, BUSD(60))).close();
BEAST_EXPECT(env.balance(A, BUSD) - aBalanceStart == BUSD(60));
BEAST_EXPECT(env.balance(C, BUSD) - cBalanceStart == BUSD(-50 - 60));
// bridge modify test cases
env.tx(bridge_modify(B, goodBridge1, XRP(33), std::nullopt)).close();
BEAST_EXPECT((*env.bridge(goodBridge1))[sfSignatureReward] == XRP(33));
env.tx(bridge_modify(A, goodBridge2, XRP(44), std::nullopt)).close();
BEAST_EXPECT((*env.bridge(goodBridge2))[sfSignatureReward] == XRP(44));
}
void
testXChainCreateBridgeMatrix()
{
using namespace jtx;
testcase("Create Bridge Matrix");
// Test all combinations of the following:`
// --------------------------------------
// - Locking chain is IOU with locking chain door account as issuer
// - Locking chain is IOU with issuing chain door account that
// exists on the locking chain as issuer
// - Locking chain is IOU with issuing chain door account that does
// not exists on the locking chain as issuer
// - Locking chain is IOU with non-door account (that exists on the
// locking chain ledger) as issuer
// - Locking chain is IOU with non-door account (that does not exist
// exists on the locking chain ledger) as issuer
// - Locking chain is XRP
// ---------------------------------------------------------------------
// - Issuing chain is IOU with issuing chain door account as the
// issuer
// - Issuing chain is IOU with locking chain door account (that
// exists on the issuing chain ledger) as the issuer
// - Issuing chain is IOU with locking chain door account (that does
// not exist on the issuing chain ledger) as the issuer
// - Issuing chain is IOU with non-door account (that exists on the
// issuing chain ledger) as the issuer
// - Issuing chain is IOU with non-door account (that does not
// exists on the issuing chain ledger) as the issuer
// - Issuing chain is XRP and issuing chain door account is not the
// root account
// - Issuing chain is XRP and issuing chain door account is the root
// account
// ---------------------------------------------------------------------
// That's 42 combinations. The only combinations that should succeed
// are:
// - Locking chain is any IOU,
// - Issuing chain is IOU with issuing chain door account as the
// issuer
// Locking chain is XRP,
// - Issuing chain is XRP with issuing chain is the root account.
// ---------------------------------------------------------------------
Account a("a"), b("b");
Issue ia, ib;
std::tuple lcs{
std::make_pair(
"Locking chain is IOU(locking chain door)",
[&](auto& env, bool) {
a = mcDoor;
ia = mcDoor["USD"];
}),
std::make_pair(
"Locking chain is IOU(issuing chain door funded on locking "
"chain)",
[&](auto& env, bool shouldFund) {
a = mcDoor;
ia = scDoor["USD"];
if (shouldFund)
env.fund(XRP(10000), scDoor);
}),
std::make_pair(
"Locking chain is IOU(issuing chain door account unfunded "
"on locking chain)",
[&](auto& env, bool) {
a = mcDoor;
ia = scDoor["USD"];
}),
std::make_pair(
"Locking chain is IOU(bob funded on locking chain)",
[&](auto& env, bool) {
a = mcDoor;
ia = mcGw["USD"];
}),
std::make_pair(
"Locking chain is IOU(bob unfunded on locking chain)",
[&](auto& env, bool) {
a = mcDoor;
ia = mcuGw["USD"];
}),
std::make_pair("Locking chain is XRP", [&](auto& env, bool) {
a = mcDoor;
ia = xrpIssue();
})};
std::tuple ics{
std::make_pair(
"Issuing chain is IOU(issuing chain door account)",
[&](auto& env, bool) {
b = scDoor;
ib = scDoor["USD"];
}),
std::make_pair(
"Issuing chain is IOU(locking chain door funded on issuing "
"chain)",
[&](auto& env, bool shouldFund) {
b = scDoor;
ib = mcDoor["USD"];
if (shouldFund)
env.fund(XRP(10000), mcDoor);
}),
std::make_pair(
"Issuing chain is IOU(locking chain door unfunded on "
"issuing chain)",
[&](auto& env, bool) {
b = scDoor;
ib = mcDoor["USD"];
}),
std::make_pair(
"Issuing chain is IOU(bob funded on issuing chain)",
[&](auto& env, bool) {
b = scDoor;
ib = mcGw["USD"];
}),
std::make_pair(
"Issuing chain is IOU(bob unfunded on issuing chain)",
[&](auto& env, bool) {
b = scDoor;
ib = mcuGw["USD"];
}),
std::make_pair(
"Issuing chain is XRP and issuing chain door account is "
"not the root account",
[&](auto& env, bool) {
b = scDoor;
ib = xrpIssue();
}),
std::make_pair(
"Issuing chain is XRP and issuing chain door account is "
"the root account ",
[&](auto& env, bool) {
b = Account::master;
ib = xrpIssue();
})};
std::vector<std::pair<int, int>> expected_result{
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{tesSUCCESS, tesSUCCESS},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{tecNO_ISSUER, tesSUCCESS},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{tesSUCCESS, tesSUCCESS},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{tecNO_ISSUER, tesSUCCESS},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{temXCHAIN_BRIDGE_BAD_ISSUES, temXCHAIN_BRIDGE_BAD_ISSUES},
{tesSUCCESS, tesSUCCESS}};
std::vector<std::tuple<TER, TER, bool>> test_result;
auto testcase = [&](auto const& lc, auto const& ic) {
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
lc.second(mcEnv, true);
lc.second(scEnv, false);
ic.second(mcEnv, false);
ic.second(scEnv, true);
auto const& expected = expected_result[test_result.size()];
mcEnv.tx(
create_bridge(a, bridge(a, ia, b, ib)),
ter(TER::fromInt(expected.first)));
TER mcTER = mcEnv.env_.ter();
scEnv.tx(
create_bridge(b, bridge(a, ia, b, ib)),
ter(TER::fromInt(expected.second)));
TER scTER = scEnv.env_.ter();
bool pass = mcTER == tesSUCCESS && scTER == tesSUCCESS;
test_result.emplace_back(mcTER, scTER, pass);
};
auto apply_ics = [&](auto const& lc, auto const& ics) {
std::apply(
[&](auto const&... ic) { (testcase(lc, ic), ...); }, ics);
};
std::apply([&](auto const&... lc) { (apply_ics(lc, ics), ...); }, lcs);
#if GENERATE_MTX_OUTPUT
// optional output of matrix results in markdown format
// ----------------------------------------------------
std::string fname{std::tmpnam(nullptr)};
fname += ".md";
std::cout << "Markdown output for matrix test: " << fname << "\n";
auto print_res = [](auto tup) -> std::string {
std::string status = std::string(transToken(std::get<0>(tup))) +
" / " + transToken(std::get<1>(tup));
if (std::get<2>(tup))
return status;
else
{
// red
return std::string("`") + status + "`";
}
};
auto output_table = [&](auto print_res) {
size_t test_idx = 0;
std::string res;
res.reserve(10000); // should be enough :-)
// first two header lines
res += "| `issuing ->` | ";
std::apply(
[&](auto const&... ic) {
((res += ic.first, res += " | "), ...);
},
ics);
res += "\n";
res += "| :--- | ";
std::apply(
[&](auto const&... ic) {
(((void)ic.first, res += ":---: | "), ...);
},
ics);
res += "\n";
auto output = [&](auto const& lc, auto const& ic) {
res += print_res(test_result[test_idx]);
res += " | ";
++test_idx;
};
auto output_ics = [&](auto const& lc, auto const& ics) {
res += "| ";
res += lc.first;
res += " | ";
std::apply(
[&](auto const&... ic) { (output(lc, ic), ...); }, ics);
res += "\n";
};
std::apply(
[&](auto const&... lc) { (output_ics(lc, ics), ...); }, lcs);
return res;
};
std::ofstream(fname) << output_table(print_res);
std::string ter_fname{std::tmpnam(nullptr)};
std::cout << "ter output for matrix test: " << ter_fname << "\n";
std::ofstream ofs(ter_fname);
for (auto& t : test_result)
{
ofs << "{ " << std::string(transToken(std::get<0>(t))) << ", "
<< std::string(transToken(std::get<1>(t))) << "}\n,";
}
#endif
}
void
testXChainModifyBridge()
{
using namespace jtx;
testcase("Modify Bridge");
// Changing a non-existent bridge should fail
XEnv(*this).tx(
bridge_modify(
mcAlice,
bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"]),
XRP(2),
std::nullopt),
ter(tecNO_ENTRY));
// must change something
// XEnv(*this)
// .tx(create_bridge(mcDoor, jvb, XRP(1), XRP(1)))
// .tx(bridge_modify(mcDoor, jvb, XRP(1), XRP(1)),
// ter(temMALFORMED));
// must change something
XEnv(*this)
.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(1)))
.close()
.tx(bridge_modify(mcDoor, jvb, {}, {}), ter(temMALFORMED));
// Reward amount is non-xrp
XEnv(*this).tx(
bridge_modify(mcDoor, jvb, mcUSD(2), XRP(10)),
ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT));
// Reward amount is XRP and negative
XEnv(*this).tx(
bridge_modify(mcDoor, jvb, XRP(-2), XRP(10)),
ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT));
// Min create amount is non-xrp
XEnv(*this).tx(
bridge_modify(mcDoor, jvb, XRP(2), mcUSD(10)),
ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));
// Min create amount is zero
XEnv(*this).tx(
bridge_modify(mcDoor, jvb, XRP(2), XRP(0)),
ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));
// Min create amount is negative
XEnv(*this).tx(
bridge_modify(mcDoor, jvb, XRP(2), XRP(-10)),
ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT));
// First check the regular claim process (without bridge_modify)
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
}
BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
}
// Check that the reward paid from a claim Id was the reward when
// the claim id was created, not the reward since the bridge was
// modified.
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
// Now modify the reward on the bridge
mcEnv.tx(bridge_modify(mcDoor, jvb, XRP(2), XRP(10))).close();
scEnv.tx(bridge_modify(Account::master, jvb, XRP(2), XRP(10)))
.close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
}
// make sure the reward accounts indeed received the original
// split reward (1 split 5 ways) instead of the updated 2 XRP.
BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
}
// Check that the signatures used to verify attestations and decide
// if there is a quorum are the current signer's list on the door
// account, not the signer's list that was in effect when the claim
// id was created.
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
// change signers - claim should not be processed is the batch
// is signed by original signers
scEnv.tx(jtx::signers(Account::master, quorum, alt_signers))
.close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
// submit claim using outdated signers - should fail
scEnv
.multiTx(
claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers),
ter(tecNO_PERMISSION))
.close();
if (withClaim)
{
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecXCHAIN_CLAIM_NO_QUORUM))
.close();
}
// make sure transfer has not happened as we sent attestations
// using outdated signers
BEAST_EXPECT(transfer.has_not_happened());
// submit claim using current signers - should succeed
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
alt_signers))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
}
// make sure the transfer went through as we sent attestations
// using new signers
BEAST_EXPECT(
transfer.has_happened(amt, split_reward_quorum, false));
}
// coverage test: bridge_modify transaction with incorrect flag
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.close()
.tx(bridge_modify(mcDoor, jvb, XRP(1), XRP(2)),
txflags(tfFillOrKill),
ter(temINVALID_FLAG));
// coverage test: bridge_modify transaction with xchain feature
// disabled
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.disableFeature(featureXChainBridge)
.close()
.tx(bridge_modify(mcDoor, jvb, XRP(1), XRP(2)), ter(temDISABLED));
// coverage test: bridge_modify return temSIDECHAIN_NONDOOR_OWNER;
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.close()
.tx(bridge_modify(mcAlice, jvb, XRP(1), XRP(2)),
ter(temXCHAIN_BRIDGE_NONDOOR_OWNER));
/**
* test tfClearAccountCreateAmount flag in BridgeModify tx
* -- tx has both minAccountCreateAmount and the flag, temMALFORMED
* -- tx has the flag and also modifies signature reward, tesSUCCESS
* -- XChainCreateAccountCommit tx fail after previous step
*/
XEnv(*this)
.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20)))
.close()
.tx(sidechain_xchain_account_create(
mcAlice, jvb, scuAlice, XRP(100), reward))
.close()
.tx(bridge_modify(mcDoor, jvb, {}, XRP(2)),
txflags(tfClearAccountCreateAmount),
ter(temMALFORMED))
.close()
.tx(bridge_modify(mcDoor, jvb, XRP(3), {}),
txflags(tfClearAccountCreateAmount))
.close()
.tx(sidechain_xchain_account_create(
mcAlice, jvb, scuBob, XRP(100), XRP(3)),
ter(tecXCHAIN_CREATE_ACCOUNT_DISABLED))
.close();
}
void
testXChainCreateClaimID()
{
using namespace jtx;
XRPAmount res1 = reserve(1);
XRPAmount tx_fee = txFee();
testcase("Create ClaimID");
// normal bridge create for sanity check with the exact necessary
// account balance
XEnv(*this, true)
.tx(create_bridge(Account::master, jvb))
.fund(res1, scuAlice) // acct reserve + 1 object
.close()
.tx(xchain_create_claim_id(scuAlice, jvb, reward, mcAlice))
.close();
// check reward not deducted when claim id is created
{
XEnv xenv(*this, true);
Balance scAlice_bal(xenv, scAlice);
xenv.tx(create_bridge(Account::master, jvb))
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
BEAST_EXPECT(scAlice_bal.diff() == -tx_fee);
}
// Non-existent bridge
XEnv(*this, true)
.tx(xchain_create_claim_id(
scAlice,
bridge(mcAlice, mcAlice["USD"], scBob, scBob["USD"]),
reward,
mcAlice),
ter(tecNO_ENTRY))
.close();
// Creating the new object would put the account below the reserve
XEnv(*this, true)
.tx(create_bridge(Account::master, jvb))
.fund(res1 - xrp_dust, scuAlice) // barely not enough
.close()
.tx(xchain_create_claim_id(scuAlice, jvb, reward, mcAlice),
ter(tecINSUFFICIENT_RESERVE))
.close();
// The specified reward doesn't match the reward on the bridge (test
// by giving the reward amount for the other side, as well as a
// completely non-matching reward)
XEnv(*this, true)
.tx(create_bridge(Account::master, jvb))
.close()
.tx(xchain_create_claim_id(
scAlice, jvb, split_reward_quorum, mcAlice),
ter(tecXCHAIN_REWARD_MISMATCH))
.close();
// A reward amount that isn't XRP
XEnv(*this, true)
.tx(create_bridge(Account::master, jvb))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, mcUSD(1), mcAlice),
ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT))
.close();
// coverage test: xchain_create_claim_id transaction with incorrect
// flag
XEnv(*this, true)
.tx(create_bridge(Account::master, jvb))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice),
txflags(tfFillOrKill),
ter(temINVALID_FLAG))
.close();
// coverage test: xchain_create_claim_id transaction with xchain
// feature disabled
XEnv(*this, true)
.tx(create_bridge(Account::master, jvb))
.disableFeature(featureXChainBridge)
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice),
ter(temDISABLED))
.close();
}
void
testXChainCommit()
{
using namespace jtx;
XRPAmount res0 = reserve(0);
XRPAmount tx_fee = txFee();
testcase("Commit");
// Commit to a non-existent bridge
XEnv(*this).tx(
xchain_commit(mcAlice, jvb, 1, one_xrp, scBob), ter(tecNO_ENTRY));
// check that reward not deducted when doing the commit
{
XEnv xenv(*this);
Balance alice_bal(xenv, mcAlice);
auto const amt = XRP(1000);
xenv.tx(create_bridge(mcDoor, jvb))
.close()
.tx(xchain_commit(mcAlice, jvb, 1, amt, scBob))
.close();
STAmount claim_cost = amt;
BEAST_EXPECT(alice_bal.diff() == -(claim_cost + tx_fee));
}
// Commit a negative amount
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.close()
.tx(xchain_commit(mcAlice, jvb, 1, XRP(-1), scBob),
ter(temBAD_AMOUNT));
// Commit an amount whose issue that does not match the expected
// issue on the bridge (either LockingChainIssue or
// IssuingChainIssue, depending on the chain).
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.close()
.tx(xchain_commit(mcAlice, jvb, 1, mcUSD(100), scBob),
ter(temBAD_ISSUER));
// Commit an amount that would put the sender below the required
// reserve (if XRP)
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.fund(res0 + one_xrp - xrp_dust, mcuAlice) // barely not enough
.close()
.tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob),
ter(tecUNFUNDED_PAYMENT));
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.fund(
res0 + one_xrp + xrp_dust, // "xrp_dust" for tx fees
mcuAlice) // exactly enough => should succeed
.close()
.tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob));
// Commit an amount above the account's balance (for both XRP and
// IOUs)
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.fund(res0, mcuAlice) // barely not enough
.close()
.tx(xchain_commit(mcuAlice, jvb, 1, res0 + one_xrp, scBob),
ter(tecUNFUNDED_PAYMENT));
auto jvb_USD = bridge(mcDoor, mcUSD, scGw, scUSD);
// commit sent from iou issuer (mcGw) succeeds - should it?
XEnv(*this)
.tx(trust(mcDoor, mcUSD(10000))) // door needs to have a trustline
.tx(create_bridge(mcDoor, jvb_USD))
.close()
.tx(xchain_commit(mcGw, jvb_USD, 1, mcUSD(1), scBob));
// commit to a door account from the door account. This should fail.
XEnv(*this)
.tx(trust(mcDoor, mcUSD(10000))) // door needs to have a trustline
.tx(create_bridge(mcDoor, jvb_USD))
.close()
.tx(xchain_commit(mcDoor, jvb_USD, 1, mcUSD(1), scBob),
ter(tecXCHAIN_SELF_COMMIT));
// commit sent from mcAlice which has no IOU balance => should fail
XEnv(*this)
.tx(trust(mcDoor, mcUSD(10000))) // door needs to have a trustline
.tx(create_bridge(mcDoor, jvb_USD))
.close()
.tx(xchain_commit(mcAlice, jvb_USD, 1, mcUSD(1), scBob),
ter(terNO_LINE));
// commit sent from mcAlice which has no IOU balance => should fail
// just changed the destination to scGw (which is the door account
// and may not make much sense)
XEnv(*this)
.tx(trust(mcDoor, mcUSD(10000))) // door needs to have a trustline
.tx(create_bridge(mcDoor, jvb_USD))
.close()
.tx(xchain_commit(mcAlice, jvb_USD, 1, mcUSD(1), scGw),
ter(terNO_LINE));
// commit sent from mcAlice which has a IOU balance => should
// succeed
XEnv(*this)
.tx(trust(mcDoor, mcUSD(10000)))
.tx(trust(mcAlice, mcUSD(10000)))
.close()
.tx(pay(mcGw, mcAlice, mcUSD(10)))
.tx(create_bridge(mcDoor, jvb_USD))
.close()
//.tx(pay(mcAlice, mcDoor, mcUSD(10)));
.tx(xchain_commit(mcAlice, jvb_USD, 1, mcUSD(10), scAlice));
// coverage test: xchain_commit transaction with incorrect flag
XEnv(*this)
.tx(create_bridge(mcDoor))
.close()
.tx(xchain_commit(mcAlice, jvb, 1, one_xrp, scBob),
txflags(tfFillOrKill),
ter(temINVALID_FLAG));
// coverage test: xchain_commit transaction with xchain feature
// disabled
XEnv(*this)
.tx(create_bridge(mcDoor))
.disableFeature(featureXChainBridge)
.close()
.tx(xchain_commit(mcAlice, jvb, 1, one_xrp, scBob),
ter(temDISABLED));
}
void
testXChainAddAttestation()
{
using namespace jtx;
testcase("Add Attestation");
XRPAmount res0 = reserve(0);
XRPAmount tx_fee = txFee();
auto multiTtxFee = [&](std::uint32_t m) -> STAmount {
return multiply(tx_fee, STAmount(m), xrpIssue());
};
// Add an attestation to a claim id that has already reached quorum.
// This should succeed and share in the reward.
// note: this is true only when either:
// 1. dest account is not specified, so transfer requires a claim
// 2. or the extra attestation is sent in the same batch as the
// one reaching quorum
for (auto withClaim : {true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
std::uint32_t const claimID = 1;
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv, Account::master, scBob, scAlice, payees, withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers,
UT_XCHAIN_DEFAULT_QUORUM))
.close();
scEnv
.tx(claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[UT_XCHAIN_DEFAULT_QUORUM],
true,
claimID,
dst,
signers[UT_XCHAIN_DEFAULT_QUORUM]))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
BEAST_EXPECT(!scEnv.claimID(jvb, claimID)); // claim id deleted
BEAST_EXPECT(scEnv.claimID(jvb) == claimID);
}
BEAST_EXPECT(transfer.has_happened(amt, split_reward_everyone));
}
// Test that signature weights are correctly handled. Assign
// signature weights of 1,2,4,4 and a quorum of 7. Check that the
// 4,4 signatures reach a quorum, the 1,2,4, reach a quorum, but the
// 4,2, 4,1 and 1,2 do not.
// 1,2,4 => should succeed
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
std::uint32_t const quorum_7 = 7;
std::vector<signer> const signers_ = [] {
constexpr int numSigners = 4;
std::uint32_t weights[] = {1, 2, 4, 4};
std::vector<signer> result;
result.reserve(numSigners);
for (int i = 0; i < numSigners; ++i)
{
using namespace std::literals;
auto const a = Account("signer_"s + std::to_string(i));
result.emplace_back(a, weights[i]);
}
return result;
}();
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum_7, signers_))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
std::uint32_t const claimID = 1;
BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
3,
withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers_,
3))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
}
BEAST_EXPECT(!scEnv.claimID(jvb, 1)); // claim id deleted
BEAST_EXPECT(transfer.has_happened(
amt, divide(reward, STAmount(3), reward.issue())));
}
// 4,4 => should succeed
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
std::uint32_t const quorum_7 = 7;
std::vector<signer> const signers_ = [] {
constexpr int numSigners = 4;
std::uint32_t weights[] = {1, 2, 4, 4};
std::vector<signer> result;
result.reserve(numSigners);
for (int i = 0; i < numSigners; ++i)
{
using namespace std::literals;
auto const a = Account("signer_"s + std::to_string(i));
result.emplace_back(a, weights[i]);
}
return result;
}();
STAmount const split_reward_ =
divide(reward, STAmount(signers_.size()), reward.issue());
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum_7, signers_))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
std::uint32_t const claimID = 1;
BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[2],
2,
withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers_,
2,
2))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
}
BEAST_EXPECT(!scEnv.claimID(jvb, claimID)); // claim id deleted
BEAST_EXPECT(transfer.has_happened(
amt, divide(reward, STAmount(2), reward.issue())));
}
// 1,2 => should fail
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
std::uint32_t const quorum_7 = 7;
std::vector<signer> const signers_ = [] {
constexpr int numSigners = 4;
std::uint32_t weights[] = {1, 2, 4, 4};
std::vector<signer> result;
result.reserve(numSigners);
for (int i = 0; i < numSigners; ++i)
{
using namespace std::literals;
auto const a = Account("signer_"s + std::to_string(i));
result.emplace_back(a, weights[i]);
}
return result;
}();
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum_7, signers_))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
std::uint32_t const claimID = 1;
BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
2,
withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers_,
2))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecXCHAIN_CLAIM_NO_QUORUM))
.close();
}
BEAST_EXPECT(
!!scEnv.claimID(jvb, claimID)); // claim id still present
BEAST_EXPECT(transfer.has_not_happened());
}
// 2,4 => should fail
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
std::uint32_t const quorum_7 = 7;
std::vector<signer> const signers_ = [] {
constexpr int numSigners = 4;
std::uint32_t weights[] = {1, 2, 4, 4};
std::vector<signer> result;
result.reserve(numSigners);
for (int i = 0; i < numSigners; ++i)
{
using namespace std::literals;
auto const a = Account("signer_"s + std::to_string(i));
result.emplace_back(a, weights[i]);
}
return result;
}();
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum_7, signers_))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
std::uint32_t const claimID = 1;
BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[1],
2,
withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers_,
2,
1))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecXCHAIN_CLAIM_NO_QUORUM))
.close();
}
BEAST_EXPECT(
!!scEnv.claimID(jvb, claimID)); // claim id still present
BEAST_EXPECT(transfer.has_not_happened());
}
// Confirm that account create transactions happen in the correct
// order. If they reach quorum out of order they should not execute
// until all the previous created transactions have occurred.
// Re-adding an attestation should move funds.
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
auto const amt = XRP(1000);
auto const amt_plus_reward = amt + reward;
{
Balance door(mcEnv, mcDoor);
Balance carol(mcEnv, mcCarol);
mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20)))
.close()
.tx(sidechain_xchain_account_create(
mcAlice, jvb, scuAlice, amt, reward))
.tx(sidechain_xchain_account_create(
mcBob, jvb, scuBob, amt, reward))
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scuCarol, amt, reward))
.close();
BEAST_EXPECT(
door.diff() ==
(multiply(amt_plus_reward, STAmount(3), xrpIssue()) -
tx_fee));
BEAST_EXPECT(carol.diff() == -(amt + reward + tx_fee));
}
scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20)))
.tx(jtx::signers(Account::master, quorum, signers))
.close();
{
// send first batch of account create attest for all 3
// account create
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 2))
.multiTx(att_create_acct_vec(3, amt, scuCarol, 2))
.multiTx(att_create_acct_vec(2, amt, scuBob, 2))
.close();
BEAST_EXPECT(door.diff() == STAmount(0));
// att_create_acct_vec return vectors of size 2, so 2*3 txns
BEAST_EXPECT(attester.diff() == -multiTtxFee(6));
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1)); // ca claim id present
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2)); // ca claim id present
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3)); // ca claim id present
BEAST_EXPECT(
scEnv.claimCount(jvb) == 0); // claim count still 0
}
{
// complete attestations for 2nd account create => should
// not complete
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
scEnv.multiTx(att_create_acct_vec(2, amt, scuBob, 3, 2))
.close();
BEAST_EXPECT(door.diff() == STAmount(0));
// att_create_acct_vec return vectors of size 3, so 3 txns
BEAST_EXPECT(attester.diff() == -multiTtxFee(3));
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2)); // ca claim id present
BEAST_EXPECT(
scEnv.claimCount(jvb) == 0); // claim count still 0
}
{
// complete attestations for 3rd account create => should
// not complete
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 3, 2))
.close();
BEAST_EXPECT(door.diff() == STAmount(0));
// att_create_acct_vec return vectors of size 3, so 3 txns
BEAST_EXPECT(attester.diff() == -multiTtxFee(3));
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3)); // ca claim id present
BEAST_EXPECT(
scEnv.claimCount(jvb) == 0); // claim count still 0
}
{
// complete attestations for 1st account create => account
// should be created
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 3, 1))
.close();
BEAST_EXPECT(door.diff() == -amt_plus_reward);
// att_create_acct_vec return vectors of size 3, so 3 txns
BEAST_EXPECT(attester.diff() == -multiTtxFee(3));
BEAST_EXPECT(scEnv.balance(scuAlice) == amt);
BEAST_EXPECT(!scEnv.caClaimID(jvb, 1)); // claim id 1 deleted
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2)); // claim id 2 present
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3)); // claim id 3 present
BEAST_EXPECT(scEnv.claimCount(jvb) == 1); // claim count now 1
}
{
// resend attestations for 3rd account create => still
// should not complete
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 3, 2))
.close();
BEAST_EXPECT(door.diff() == STAmount(0));
// att_create_acct_vec return vectors of size 3, so 3 txns
BEAST_EXPECT(attester.diff() == -multiTtxFee(3));
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 2)); // claim id 2 present
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3)); // claim id 3 present
BEAST_EXPECT(
scEnv.claimCount(jvb) == 1); // claim count still 1
}
{
// resend attestations for 2nd account create => account
// should be created
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
scEnv.multiTx(att_create_acct_vec(2, amt, scuBob, 1)).close();
BEAST_EXPECT(door.diff() == -amt_plus_reward);
BEAST_EXPECT(attester.diff() == -tx_fee);
BEAST_EXPECT(scEnv.balance(scuBob) == amt);
BEAST_EXPECT(!scEnv.caClaimID(jvb, 2)); // claim id 2 deleted
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 3)); // claim id 3 present
BEAST_EXPECT(scEnv.claimCount(jvb) == 2); // claim count now 2
}
{
// resend attestations for 3rc account create => account
// should be created
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 1)).close();
BEAST_EXPECT(door.diff() == -amt_plus_reward);
BEAST_EXPECT(attester.diff() == -tx_fee);
BEAST_EXPECT(scEnv.balance(scuCarol) == amt);
BEAST_EXPECT(!scEnv.caClaimID(jvb, 3)); // claim id 3 deleted
BEAST_EXPECT(scEnv.claimCount(jvb) == 3); // claim count now 3
}
}
// Check that creating an account with less than the minimum reserve
// fails.
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
auto const amt = res0 - XRP(1);
auto const amt_plus_reward = amt + reward;
mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))).close();
{
Balance door(mcEnv, mcDoor);
Balance carol(mcEnv, mcCarol);
mcEnv
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scuAlice, amt, reward))
.close();
BEAST_EXPECT(door.diff() == amt_plus_reward);
BEAST_EXPECT(carol.diff() == -(amt_plus_reward + tx_fee));
}
scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20)))
.tx(jtx::signers(Account::master, quorum, signers))
.close();
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 2)).close();
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1)); // claim id present
BEAST_EXPECT(
scEnv.claimCount(jvb) == 0); // claim count is one less
scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 2, 2)).close();
BEAST_EXPECT(!scEnv.caClaimID(jvb, 1)); // claim id deleted
BEAST_EXPECT(
scEnv.claimCount(jvb) == 1); // claim count was incremented
BEAST_EXPECT(attester.diff() == -multiTtxFee(4));
BEAST_EXPECT(door.diff() == -reward);
BEAST_EXPECT(!scEnv.account(scuAlice));
}
// Check that sending funds with an account create txn to an
// existing account works.
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
auto const amt = XRP(111);
auto const amt_plus_reward = amt + reward;
mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))).close();
{
Balance door(mcEnv, mcDoor);
Balance carol(mcEnv, mcCarol);
mcEnv
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scAlice, amt, reward))
.close();
BEAST_EXPECT(door.diff() == amt_plus_reward);
BEAST_EXPECT(carol.diff() == -(amt_plus_reward + tx_fee));
}
scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20)))
.tx(jtx::signers(Account::master, quorum, signers))
.close();
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
Balance alice(scEnv, scAlice);
scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2)).close();
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1)); // claim id present
BEAST_EXPECT(
scEnv.claimCount(jvb) == 0); // claim count is one less
scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2, 2)).close();
BEAST_EXPECT(!scEnv.caClaimID(jvb, 1)); // claim id deleted
BEAST_EXPECT(
scEnv.claimCount(jvb) == 1); // claim count was incremented
BEAST_EXPECT(door.diff() == -amt_plus_reward);
BEAST_EXPECT(attester.diff() == -multiTtxFee(4));
BEAST_EXPECT(alice.diff() == amt);
}
// Check that sending funds to an existing account with deposit auth
// set fails for account create transactions.
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
auto const amt = XRP(1000);
auto const amt_plus_reward = amt + reward;
mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20))).close();
{
Balance door(mcEnv, mcDoor);
Balance carol(mcEnv, mcCarol);
mcEnv
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scAlice, amt, reward))
.close();
BEAST_EXPECT(door.diff() == amt_plus_reward);
BEAST_EXPECT(carol.diff() == -(amt_plus_reward + tx_fee));
}
scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20)))
.tx(jtx::signers(Account::master, quorum, signers))
.tx(fset("scAlice", asfDepositAuth)) // set deposit auth
.close();
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
Balance alice(scEnv, scAlice);
scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2)).close();
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1)); // claim id present
BEAST_EXPECT(
scEnv.claimCount(jvb) == 0); // claim count is one less
scEnv.multiTx(att_create_acct_vec(1, amt, scAlice, 2, 2)).close();
BEAST_EXPECT(!scEnv.caClaimID(jvb, 1)); // claim id deleted
BEAST_EXPECT(
scEnv.claimCount(jvb) == 1); // claim count was incremented
BEAST_EXPECT(door.diff() == -reward);
BEAST_EXPECT(attester.diff() == -multiTtxFee(4));
BEAST_EXPECT(alice.diff() == STAmount(0));
}
// If an account is unable to pay the reserve, check that it fails.
// [greg todo] I don't know what this should test??
// If an attestation already exists for that server and claim id,
// the new attestation should replace the old attestation
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
auto const amt = XRP(1000);
auto const amt_plus_reward = amt + reward;
{
Balance door(mcEnv, mcDoor);
Balance carol(mcEnv, mcCarol);
mcEnv.tx(create_bridge(mcDoor, jvb, reward, XRP(20)))
.close()
.tx(sidechain_xchain_account_create(
mcAlice, jvb, scuAlice, amt, reward))
.close() // make sure Alice gets claim #1
.tx(sidechain_xchain_account_create(
mcBob, jvb, scuBob, amt, reward))
.close() // make sure Bob gets claim #2
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scuCarol, amt, reward))
.close(); // and Carol will get claim #3
BEAST_EXPECT(
door.diff() ==
(multiply(amt_plus_reward, STAmount(3), xrpIssue()) -
tx_fee));
BEAST_EXPECT(carol.diff() == -(amt + reward + tx_fee));
}
std::uint32_t const red_quorum = 2;
scEnv.tx(create_bridge(Account::master, jvb, reward, XRP(20)))
.tx(jtx::signers(Account::master, red_quorum, signers))
.close();
{
Balance attester(scEnv, scAttester);
Balance door(scEnv, Account::master);
auto const bad_amt = XRP(10);
std::uint32_t txCount = 0;
// send attestations with incorrect amounts to for all 3
// AccountCreate. They will be replaced later
scEnv.multiTx(att_create_acct_vec(1, bad_amt, scuAlice, 1))
.multiTx(att_create_acct_vec(2, bad_amt, scuBob, 1, 2))
.multiTx(att_create_acct_vec(3, bad_amt, scuCarol, 1, 1))
.close();
txCount += 3;
BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 1), "claim id 1 created");
BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 2), "claim id 2 created");
BEAST_EXPECTS(!!scEnv.caClaimID(jvb, 3), "claim id 3 created");
// note: if we send inconsistent attestations in the same
// batch, the transaction errors.
// from now on we send correct attestations
scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 1, 0))
.multiTx(att_create_acct_vec(2, amt, scuBob, 1, 2))
.multiTx(att_create_acct_vec(3, amt, scuCarol, 1, 4))
.close();
txCount += 3;
BEAST_EXPECTS(
!!scEnv.caClaimID(jvb, 1), "claim id 1 still there");
BEAST_EXPECTS(
!!scEnv.caClaimID(jvb, 2), "claim id 2 still there");
BEAST_EXPECTS(
!!scEnv.caClaimID(jvb, 3), "claim id 3 still there");
BEAST_EXPECTS(
scEnv.claimCount(jvb) == 0, "No account created yet");
scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 1, 1))
.close();
txCount += 1;
BEAST_EXPECTS(
!!scEnv.caClaimID(jvb, 3), "claim id 3 still there");
BEAST_EXPECTS(
scEnv.claimCount(jvb) == 0, "No account created yet");
scEnv.multiTx(att_create_acct_vec(1, amt, scuAlice, 1, 2))
.close();
txCount += 1;
BEAST_EXPECTS(!scEnv.caClaimID(jvb, 1), "claim id 1 deleted");
BEAST_EXPECTS(scEnv.claimCount(jvb) == 1, "scuAlice created");
scEnv.multiTx(att_create_acct_vec(2, amt, scuBob, 1, 3))
.multiTx(
att_create_acct_vec(1, amt, scuAlice, 1, 3),
ter(tecXCHAIN_ACCOUNT_CREATE_PAST))
.close();
txCount += 2;
BEAST_EXPECTS(!scEnv.caClaimID(jvb, 2), "claim id 2 deleted");
BEAST_EXPECTS(!scEnv.caClaimID(jvb, 1), "claim id 1 not added");
BEAST_EXPECTS(
scEnv.claimCount(jvb) == 2, "scuAlice & scuBob created");
scEnv.multiTx(att_create_acct_vec(3, amt, scuCarol, 1, 0))
.close();
txCount += 1;
BEAST_EXPECTS(!scEnv.caClaimID(jvb, 3), "claim id 3 deleted");
BEAST_EXPECTS(
scEnv.claimCount(jvb) == 3, "All 3 accounts created");
// because of the division of the rewards among attesters,
// sometimes a couple drops are left over unspent in the
// door account (here 2 drops)
BEAST_EXPECT(
multiply(amt_plus_reward, STAmount(3), xrpIssue()) +
door.diff() <
drops(3));
BEAST_EXPECT(attester.diff() == -multiTtxFee(txCount));
BEAST_EXPECT(scEnv.balance(scuAlice) == amt);
BEAST_EXPECT(scEnv.balance(scuBob) == amt);
BEAST_EXPECT(scEnv.balance(scuCarol) == amt);
}
}
// If attestation moves funds, confirm the claim ledger objects are
// removed (for both account create and "regular" transactions)
// [greg] we do this in all attestation tests
// coverage test: add_attestation transaction with incorrect flag
{
XEnv scEnv(*this, true);
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(claim_attestation(
scAttester,
jvb,
mcAlice,
XRP(1000),
payees[0],
true,
1,
{},
signers[0]),
txflags(tfFillOrKill),
ter(temINVALID_FLAG))
.close();
}
// coverage test: add_attestation with xchain feature
// disabled
{
XEnv scEnv(*this, true);
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.disableFeature(featureXChainBridge)
.close()
.tx(claim_attestation(
scAttester,
jvb,
mcAlice,
XRP(1000),
payees[0],
true,
1,
{},
signers[0]),
ter(temDISABLED))
.close();
}
}
void
testXChainAddClaimNonBatchAttestation()
{
using namespace jtx;
testcase("Add Non Batch Claim Attestation");
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
std::uint32_t const claimID = 1;
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present
Account const dst{scBob};
auto const amt = XRP(1000);
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
auto const dstStartBalance = scEnv.env_.balance(dst);
for (int i = 0; i < signers.size(); ++i)
{
auto const att = claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[i],
true,
claimID,
dst,
signers[i]);
TER const expectedTER =
i < quorum ? tesSUCCESS : TER{tecXCHAIN_NO_CLAIM_ID};
if (i + 1 == quorum)
scEnv.tx(att, ter(expectedTER)).close();
else
scEnv.tx(att, ter(expectedTER)).close();
if (i + 1 < quorum)
BEAST_EXPECT(dstStartBalance == scEnv.env_.balance(dst));
else
BEAST_EXPECT(
dstStartBalance + amt == scEnv.env_.balance(dst));
}
BEAST_EXPECT(dstStartBalance + amt == scEnv.env_.balance(dst));
}
{
/**
* sfAttestationSignerAccount related cases.
*
* Good cases:
* --G1: master key
* --G2: regular key
* --G3: public key and non-exist (unfunded) account match
*
* Bad cases:
* --B1: disabled master key
* --B2: single item signer list
* --B3: public key and non-exist (unfunded) account mismatch
* --B4: not on signer list
* --B5: missing sfAttestationSignerAccount field
*/
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
for (auto i = 0; i < UT_XCHAIN_DEFAULT_NUM_SIGNERS - 2; ++i)
scEnv.fund(amt, alt_signers[i].account);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, alt_signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
Account const dst{scBob};
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
auto const dstStartBalance = scEnv.env_.balance(dst);
{
// G1: master key
auto att = claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[0],
true,
claimID,
dst,
alt_signers[0]);
scEnv.tx(att).close();
}
{
// G2: regular key
// alt_signers[0] is the regular key of alt_signers[1]
// There should be 2 attestations after the transaction
scEnv
.tx(jtx::regkey(
alt_signers[1].account, alt_signers[0].account))
.close();
auto att = claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[1],
true,
claimID,
dst,
alt_signers[0]);
att[sfAttestationSignerAccount.getJsonName()] =
alt_signers[1].account.human();
scEnv.tx(att).close();
}
{
// B3: public key and non-exist (unfunded) account mismatch
// G3: public key and non-exist (unfunded) account match
auto const unfundedSigner1 =
alt_signers[UT_XCHAIN_DEFAULT_NUM_SIGNERS - 1];
auto const unfundedSigner2 =
alt_signers[UT_XCHAIN_DEFAULT_NUM_SIGNERS - 2];
auto att = claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[UT_XCHAIN_DEFAULT_NUM_SIGNERS - 1],
true,
claimID,
dst,
unfundedSigner1);
att[sfAttestationSignerAccount.getJsonName()] =
unfundedSigner2.account.human();
scEnv.tx(att, ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR))
.close();
att[sfAttestationSignerAccount.getJsonName()] =
unfundedSigner1.account.human();
scEnv.tx(att).close();
}
{
// B2: single item signer list
std::vector<signer> tempSignerList = {signers[0]};
scEnv.tx(
jtx::signers(alt_signers[2].account, 1, tempSignerList));
auto att = claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[2],
true,
claimID,
dst,
tempSignerList.front());
att[sfAttestationSignerAccount.getJsonName()] =
alt_signers[2].account.human();
scEnv.tx(att, ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR))
.close();
}
{
// B1: disabled master key
scEnv.tx(fset(alt_signers[2].account, asfDisableMaster, 0))
.close();
auto att = claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[2],
true,
claimID,
dst,
alt_signers[2]);
scEnv.tx(att, ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR))
.close();
}
{
// --B4: not on signer list
auto att = claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[0],
true,
claimID,
dst,
signers[0]);
scEnv.tx(att, ter(tecNO_PERMISSION)).close();
}
{
// --B5: missing sfAttestationSignerAccount field
// Then submit the one with the field. Should rearch quorum.
auto att = claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[3],
true,
claimID,
dst,
alt_signers[3]);
att.removeMember(sfAttestationSignerAccount.getJsonName());
scEnv.tx(att, ter(temMALFORMED)).close();
BEAST_EXPECT(dstStartBalance == scEnv.env_.balance(dst));
att[sfAttestationSignerAccount.getJsonName()] =
alt_signers[3].account.human();
scEnv.tx(att).close();
BEAST_EXPECT(dstStartBalance + amt == scEnv.env_.balance(dst));
}
}
}
void
testXChainAddAccountCreateNonBatchAttestation()
{
using namespace jtx;
testcase("Add Non Batch Account Create Attestation");
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
XRPAmount tx_fee = mcEnv.txFee();
Account a{"a"};
Account doorA{"doorA"};
STAmount funds{XRP(10000)};
mcEnv.fund(funds, a);
mcEnv.fund(funds, doorA);
Account ua{"ua"}; // unfunded account we want to create
BridgeDef xrp_b{
doorA,
xrpIssue(),
Account::master,
xrpIssue(),
XRP(1), // reward
XRP(20), // minAccountCreate
4, // quorum
signers,
Json::nullValue};
xrp_b.initBridge(mcEnv, scEnv);
auto const amt = XRP(777);
auto const amt_plus_reward = amt + xrp_b.reward;
{
Balance bal_doorA(mcEnv, doorA);
Balance bal_a(mcEnv, a);
mcEnv
.tx(sidechain_xchain_account_create(
a, xrp_b.jvb, ua, amt, xrp_b.reward))
.close();
BEAST_EXPECT(bal_doorA.diff() == amt_plus_reward);
BEAST_EXPECT(bal_a.diff() == -(amt_plus_reward + tx_fee));
}
for (int i = 0; i < signers.size(); ++i)
{
auto const att = create_account_attestation(
signers[0].account,
xrp_b.jvb,
a,
amt,
xrp_b.reward,
signers[i].account,
true,
1,
ua,
signers[i]);
TER const expectedTER = i < xrp_b.quorum
? tesSUCCESS
: TER{tecXCHAIN_ACCOUNT_CREATE_PAST};
scEnv.tx(att, ter(expectedTER)).close();
if (i + 1 < xrp_b.quorum)
BEAST_EXPECT(!scEnv.env_.le(ua));
else
BEAST_EXPECT(scEnv.env_.le(ua));
}
BEAST_EXPECT(scEnv.env_.le(ua));
}
void
testXChainClaim()
{
using namespace jtx;
XRPAmount res0 = reserve(0);
XRPAmount tx_fee = txFee();
testcase("Claim");
// Claim where the amount matches what is attested to, to an account
// that exists, and there are enough attestations to reach a quorum
// => should succeed
// -----------------------------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
}
BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
}
// Claim with just one attestation signed by the Master key
// => should not succeed
// -----------------------------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv
.tx(create_bridge(Account::master, jvb))
//.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
1,
withClaim);
jtx::signer master_signer(Account::master);
scEnv
.tx(claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[0],
true,
claimID,
dst,
master_signer),
ter(tecXCHAIN_NO_SIGNERS_LIST))
.close();
BEAST_EXPECT(transfer.has_not_happened());
}
// Claim with just one attestation signed by a regular key
// associated to the master account
// => should not succeed
// -----------------------------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv
.tx(create_bridge(Account::master, jvb))
//.tx(jtx::signers(Account::master, quorum, signers))
.tx(jtx::regkey(Account::master, payees[0]))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
1,
withClaim);
jtx::signer master_signer(payees[0]);
scEnv
.tx(claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[0],
true,
claimID,
dst,
master_signer),
ter(tecXCHAIN_NO_SIGNERS_LIST))
.close();
BEAST_EXPECT(transfer.has_not_happened());
}
// Claim against non-existent bridge
// ---------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
auto jvb_unknown =
bridge(mcBob, xrpIssue(), Account::master, xrpIssue());
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(
scAlice, jvb_unknown, reward, mcAlice),
ter(tecNO_ENTRY))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv
.tx(xchain_commit(mcAlice, jvb_unknown, claimID, amt, dst),
ter(tecNO_ENTRY))
.close();
BalanceTransfer transfer(
scEnv, Account::master, scBob, scAlice, payees, withClaim);
scEnv
.tx(claim_attestation(
scAttester,
jvb_unknown,
mcAlice,
amt,
payees[0],
true,
claimID,
dst,
signers[0]),
ter(tecNO_ENTRY))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb_unknown, claimID, amt, scBob),
ter(tecNO_ENTRY))
.close();
}
BEAST_EXPECT(transfer.has_not_happened());
}
// Claim against non-existent claim id
// -----------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv, Account::master, scBob, scAlice, payees, withClaim);
// attest using non-existent claim id
scEnv
.tx(claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[0],
true,
999,
dst,
signers[0]),
ter(tecXCHAIN_NO_CLAIM_ID))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// claim using non-existent claim id
scEnv
.tx(xchain_claim(scAlice, jvb, 999, amt, scBob),
ter(tecXCHAIN_NO_CLAIM_ID))
.close();
}
BEAST_EXPECT(transfer.has_not_happened());
}
// Claim against a claim id owned by another account
// -------------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// submit a claim transaction with the wrong account (scGw
// instead of scAlice)
scEnv
.tx(xchain_claim(scGw, jvb, claimID, amt, scBob),
ter(tecXCHAIN_BAD_CLAIM_ID))
.close();
BEAST_EXPECT(transfer.has_not_happened());
}
else
{
BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
}
}
// Claim against a claim id with no attestations
// ---------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv, Account::master, scBob, scAlice, payees, withClaim);
// don't send any attestations
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecXCHAIN_CLAIM_NO_QUORUM))
.close();
}
BEAST_EXPECT(transfer.has_not_happened());
}
// Claim against a claim id with attestations, but not enough to
// make a quorum
// --------------------------------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv, Account::master, scBob, scAlice, payees, withClaim);
auto tooFew = quorum - 1;
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers,
tooFew))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecXCHAIN_CLAIM_NO_QUORUM))
.close();
}
BEAST_EXPECT(transfer.has_not_happened());
}
// Claim id of zero
// ----------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv, Account::master, scBob, scAlice, payees, withClaim);
scEnv
.multiTx(
claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
0,
dst,
signers),
ter(tecXCHAIN_NO_CLAIM_ID))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, 0, amt, scBob),
ter(tecXCHAIN_NO_CLAIM_ID))
.close();
}
BEAST_EXPECT(transfer.has_not_happened());
}
// Claim issue that does not match the expected issue on the bridge
// (either LockingChainIssue or IssuingChainIssue, depending on the
// chain). The claim id should already have enough attestations to
// reach a quorum for this amount (for a different issuer).
// ---------------------------------------------------------------------
for (auto withClaim : {true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, scUSD(1000), scBob),
ter(temBAD_AMOUNT))
.close();
}
BEAST_EXPECT(transfer.has_not_happened());
}
// Claim to a destination that does not already exist on the chain
// -----------------------------------------------------------------
for (auto withClaim : {true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scuBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scuBob),
ter(tecNO_DST))
.close();
}
BEAST_EXPECT(transfer.has_not_happened());
}
// Claim where the claim id owner does not have enough XRP to pay
// the reward
// ------------------------------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
STAmount huge_reward{XRP(20000)};
BEAST_EXPECT(huge_reward > scEnv.balance(scAlice));
scEnv.tx(create_bridge(Account::master, jvb, huge_reward))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, huge_reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
if (withClaim)
{
scEnv
.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers))
.close();
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecUNFUNDED_PAYMENT))
.close();
}
else
{
auto txns = claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers);
for (int i = 0; i < UT_XCHAIN_DEFAULT_QUORUM - 1; ++i)
{
scEnv.tx(txns[i]).close();
}
scEnv.tx(txns.back());
scEnv.close();
// The attestation should succeed, because it adds an
// attestation, but the claim should fail with insufficient
// funds
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecUNFUNDED_PAYMENT))
.close();
}
BEAST_EXPECT(transfer.has_not_happened());
}
// Claim where the claim id owner has enough XRP to pay the reward,
// but it would put his balance below the reserve
// --------------------------------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.fund(
res0 + reward,
scuAlice) // just not enough because of fees
.close()
.tx(xchain_create_claim_id(scuAlice, jvb, reward, mcAlice),
ter(tecINSUFFICIENT_RESERVE))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv, Account::master, scBob, scuAlice, payees, withClaim);
scEnv
.tx(claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[0],
true,
claimID,
dst,
signers[0]),
ter(tecXCHAIN_NO_CLAIM_ID))
.close();
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scuAlice, jvb, claimID, amt, scBob),
ter(tecXCHAIN_NO_CLAIM_ID))
.close();
}
BEAST_EXPECT(transfer.has_not_happened());
}
// Pay to an account with deposit auth set
// ---------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.tx(fset("scBob", asfDepositAuth)) // set deposit auth
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
auto txns = claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers);
for (int i = 0; i < UT_XCHAIN_DEFAULT_QUORUM - 1; ++i)
{
scEnv.tx(txns[i]).close();
}
if (withClaim)
{
scEnv.tx(txns.back()).close();
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecNO_PERMISSION))
.close();
// the transfer failed, but check that we can still use the
// claimID with a different account
Balance scCarol_bal(scEnv, scCarol);
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scCarol))
.close();
BEAST_EXPECT(scCarol_bal.diff() == amt);
}
else
{
scEnv.tx(txns.back()).close();
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecNO_PERMISSION))
.close();
// A way would be to remove deposit auth and resubmit the
// attestations (even though the witness servers won't do
// it)
scEnv
.tx(fset("scBob", 0, asfDepositAuth)) // clear deposit auth
.close();
Balance scBob_bal(scEnv, scBob);
scEnv.tx(txns.back()).close();
BEAST_EXPECT(scBob_bal.diff() == amt);
}
}
// Pay to an account with Destination Tag set
// ------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.tx(fset("scBob", asfRequireDest)) // set dest tag
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
auto txns = claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers);
for (int i = 0; i < UT_XCHAIN_DEFAULT_QUORUM - 1; ++i)
{
scEnv.tx(txns[i]).close();
}
if (withClaim)
{
scEnv.tx(txns.back()).close();
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecDST_TAG_NEEDED))
.close();
// the transfer failed, but check that we can still use the
// claimID with a different account
Balance scCarol_bal(scEnv, scCarol);
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scCarol))
.close();
BEAST_EXPECT(scCarol_bal.diff() == amt);
}
else
{
scEnv.tx(txns.back()).close();
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob),
ter(tecDST_TAG_NEEDED))
.close();
// A way would be to remove the destination tag requirement
// and resubmit the attestations (even though the witness
// servers won't do it)
scEnv
.tx(fset("scBob", 0, asfRequireDest)) // clear dest tag
.close();
Balance scBob_bal(scEnv, scBob);
scEnv.tx(txns.back()).close();
BEAST_EXPECT(scBob_bal.diff() == amt);
}
}
// Pay to an account with deposit auth set. Check that the attestations
// are still validated and that we can used the claimID to transfer the
// funds to a different account (which doesn't have deposit auth set)
// --------------------------------------------------------------------
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.tx(fset("scBob", asfDepositAuth)) // set deposit auth
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
// we should be able to submit the attestations, but the transfer
// should not occur because dest account has deposit auth set
Balance scBob_bal(scEnv, scBob);
scEnv.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers));
BEAST_EXPECT(scBob_bal.diff() == STAmount(0));
// Check that check that we still can use the claimID to transfer
// the amount to a different account
Balance scCarol_bal(scEnv, scCarol);
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scCarol)).close();
BEAST_EXPECT(scCarol_bal.diff() == amt);
}
// Claim where the amount different from what is attested to
// ---------------------------------------------------------
for (auto withClaim : {true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
scEnv.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers));
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// claim wrong amount
scEnv
.tx(xchain_claim(scAlice, jvb, claimID, one_xrp, scBob),
ter(tecXCHAIN_CLAIM_NO_QUORUM))
.close();
}
BEAST_EXPECT(transfer.has_not_happened());
}
// Verify that rewards are paid from the account that owns the claim
// id
// --------------------------------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
Balance scAlice_bal(scEnv, scAlice);
scEnv.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers));
STAmount claim_cost = reward;
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
claim_cost += tx_fee;
}
BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
BEAST_EXPECT(
scAlice_bal.diff() == -claim_cost); // because reward % 4 == 0
}
// Verify that if a reward is not evenly divisible among the reward
// accounts, the remaining amount goes to the claim id owner.
// ----------------------------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb, tiny_reward)).close();
scEnv.tx(create_bridge(Account::master, jvb, tiny_reward))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, tiny_reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM,
withClaim);
Balance scAlice_bal(scEnv, scAlice);
scEnv.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers));
STAmount claim_cost = tiny_reward;
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
claim_cost += tx_fee;
}
BEAST_EXPECT(transfer.has_happened(amt, tiny_reward_split));
BEAST_EXPECT(
scAlice_bal.diff() == -(claim_cost - tiny_reward_remainder));
}
// If a reward distribution fails for one of the reward accounts
// (the reward account doesn't exist or has deposit auth set), then
// the txn should still succeed, but that portion should go to the
// claim id owner.
// -------------------------------------------------------------------
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
std::vector<Account> alt_payees{payees.begin(), payees.end() - 1};
alt_payees.back() = Account("inexistent");
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM - 1,
withClaim);
scEnv.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
alt_payees,
true,
claimID,
dst,
signers));
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
}
// this also checks that only 3 * split_reward was deducted from
// scAlice (the payor account), since we passed alt_payees to
// BalanceTransfer
BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
}
for (auto withClaim : {false, true})
{
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
mcEnv.tx(create_bridge(mcDoor, jvb)).close();
auto& unpaid = payees[UT_XCHAIN_DEFAULT_QUORUM - 1];
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.tx(fset(unpaid, asfDepositAuth))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto dst(withClaim ? std::nullopt : std::optional<Account>{scBob});
auto const amt = XRP(1000);
std::uint32_t const claimID = 1;
mcEnv.tx(xchain_commit(mcAlice, jvb, claimID, amt, dst)).close();
// balance of last signer should not change (has deposit auth)
Balance last_signer(scEnv, unpaid);
// make sure all signers except the last one get the
// split_reward
BalanceTransfer transfer(
scEnv,
Account::master,
scBob,
scAlice,
&payees[0],
UT_XCHAIN_DEFAULT_QUORUM - 1,
withClaim);
scEnv.multiTx(claim_attestations(
scAttester,
jvb,
mcAlice,
amt,
payees,
true,
claimID,
dst,
signers));
if (withClaim)
{
BEAST_EXPECT(transfer.has_not_happened());
// need to submit a claim transactions
scEnv.tx(xchain_claim(scAlice, jvb, claimID, amt, scBob))
.close();
}
// this also checks that only 3 * split_reward was deducted from
// scAlice (the payor account), since we passed payees.size() -
// 1 to BalanceTransfer
BEAST_EXPECT(transfer.has_happened(amt, split_reward_quorum));
// and make sure the account with deposit auth received nothing
BEAST_EXPECT(last_signer.diff() == STAmount(0));
}
// coverage test: xchain_claim transaction with incorrect flag
XEnv(*this, true)
.tx(create_bridge(Account::master, jvb))
.close()
.tx(xchain_claim(scAlice, jvb, 1, XRP(1000), scBob),
txflags(tfFillOrKill),
ter(temINVALID_FLAG))
.close();
// coverage test: xchain_claim transaction with xchain feature
// disabled
XEnv(*this, true)
.tx(create_bridge(Account::master, jvb))
.disableFeature(featureXChainBridge)
.close()
.tx(xchain_claim(scAlice, jvb, 1, XRP(1000), scBob),
ter(temDISABLED))
.close();
// coverage test: XChainClaim::preclaim - isLockingChain = true;
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.close()
.tx(xchain_claim(mcAlice, jvb, 1, XRP(1000), mcBob),
ter(tecXCHAIN_NO_CLAIM_ID));
}
void
testXChainCreateAccount()
{
using namespace jtx;
testcase("Bridge Create Account");
XRPAmount tx_fee = txFee();
// coverage test: transferHelper() - dst == src
{
XEnv scEnv(*this, true);
auto const amt = XRP(111);
auto const amt_plus_reward = amt + reward;
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close();
Balance door(scEnv, Account::master);
// scEnv.tx(att_create_acct_batch1(1, amt,
// Account::master)).close();
scEnv.multiTx(att_create_acct_vec(1, amt, Account::master, 2))
.close();
BEAST_EXPECT(!!scEnv.caClaimID(jvb, 1)); // claim id present
BEAST_EXPECT(
scEnv.claimCount(jvb) == 0); // claim count is one less
// scEnv.tx(att_create_acct_batch2(1, amt,
// Account::master)).close();
scEnv.multiTx(att_create_acct_vec(1, amt, Account::master, 2, 2))
.close();
BEAST_EXPECT(!scEnv.caClaimID(jvb, 1)); // claim id deleted
BEAST_EXPECT(
scEnv.claimCount(jvb) == 1); // claim count was incremented
BEAST_EXPECT(door.diff() == -reward);
}
// Check that creating an account with less than the minimum create
// amount fails.
{
XEnv mcEnv(*this);
mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();
Balance door(mcEnv, mcDoor);
Balance carol(mcEnv, mcCarol);
mcEnv
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scuAlice, XRP(19), reward),
ter(tecXCHAIN_INSUFF_CREATE_AMOUNT))
.close();
BEAST_EXPECT(door.diff() == STAmount(0));
BEAST_EXPECT(carol.diff() == -tx_fee);
}
// Check that creating an account with invalid flags fails.
{
XEnv mcEnv(*this);
mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();
Balance door(mcEnv, mcDoor);
mcEnv
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scuAlice, XRP(20), reward),
txflags(tfFillOrKill),
ter(temINVALID_FLAG))
.close();
BEAST_EXPECT(door.diff() == STAmount(0));
}
// Check that creating an account with the XChainBridge feature
// disabled fails.
{
XEnv mcEnv(*this);
mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();
Balance door(mcEnv, mcDoor);
mcEnv.disableFeature(featureXChainBridge)
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scuAlice, XRP(20), reward),
ter(temDISABLED))
.close();
BEAST_EXPECT(door.diff() == STAmount(0));
}
// Check that creating an account with a negative amount fails
{
XEnv mcEnv(*this);
mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();
Balance door(mcEnv, mcDoor);
mcEnv
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scuAlice, XRP(-20), reward),
ter(temBAD_AMOUNT))
.close();
BEAST_EXPECT(door.diff() == STAmount(0));
}
// Check that creating an account with a negative reward fails
{
XEnv mcEnv(*this);
mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();
Balance door(mcEnv, mcDoor);
mcEnv
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scuAlice, XRP(20), XRP(-1)),
ter(temBAD_AMOUNT))
.close();
BEAST_EXPECT(door.diff() == STAmount(0));
}
// Check that door account can't lock funds onto itself
{
XEnv mcEnv(*this);
mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();
Balance door(mcEnv, mcDoor);
mcEnv
.tx(sidechain_xchain_account_create(
mcDoor, jvb, scuAlice, XRP(20), XRP(1)),
ter(tecXCHAIN_SELF_COMMIT))
.close();
BEAST_EXPECT(door.diff() == -tx_fee);
}
// Check that reward matches the amount specified in bridge
{
XEnv mcEnv(*this);
mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(20))).close();
Balance door(mcEnv, mcDoor);
mcEnv
.tx(sidechain_xchain_account_create(
mcCarol, jvb, scuAlice, XRP(20), XRP(2)),
ter(tecXCHAIN_REWARD_MISMATCH))
.close();
BEAST_EXPECT(door.diff() == STAmount(0));
}
}
void
testFeeDipsIntoReserve()
{
using namespace jtx;
XRPAmount res0 = reserve(0);
XRPAmount tx_fee = txFee();
testcase("Fee dips into reserve");
// commit where the fee dips into the reserve, this should succeed
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.fund(res0 + one_xrp + tx_fee - drops(1), mcuAlice)
.close()
.tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob),
ter(tesSUCCESS));
// commit where the commit amount drips into the reserve, this should
// fail
XEnv(*this)
.tx(create_bridge(mcDoor, jvb))
.fund(res0 + one_xrp - drops(1), mcuAlice)
.close()
.tx(xchain_commit(mcuAlice, jvb, 1, one_xrp, scBob),
ter(tecUNFUNDED_PAYMENT));
auto const minAccountCreate = XRP(20);
// account create commit where the fee dips into the reserve,
// this should succeed
XEnv(*this)
.tx(create_bridge(mcDoor, jvb, reward, minAccountCreate))
.fund(
res0 + tx_fee + minAccountCreate + reward - drops(1), mcuAlice)
.close()
.tx(sidechain_xchain_account_create(
mcuAlice, jvb, scuAlice, minAccountCreate, reward),
ter(tesSUCCESS));
// account create commit where the commit dips into the reserve,
// this should fail
XEnv(*this)
.tx(create_bridge(mcDoor, jvb, reward, minAccountCreate))
.fund(res0 + minAccountCreate + reward - drops(1), mcuAlice)
.close()
.tx(sidechain_xchain_account_create(
mcuAlice, jvb, scuAlice, minAccountCreate, reward),
ter(tecUNFUNDED_PAYMENT));
}
void
testXChainDeleteDoor()
{
using namespace jtx;
testcase("Bridge Delete Door Account");
auto const acctDelFee{
drops(XEnv(*this).env_.current()->fees().increment)};
// Deleting an account that owns bridge should fail
{
XEnv mcEnv(*this);
mcEnv.tx(create_bridge(mcDoor, jvb, XRP(1), XRP(1))).close();
// We don't allow an account to be deleted if its sequence
// number is within 256 of the current ledger.
for (size_t i = 0; i < 256; ++i)
mcEnv.close();
// try to delete mcDoor, send funds to mcAlice
mcEnv.tx(
acctdelete(mcDoor, mcAlice),
fee(acctDelFee),
ter(tecHAS_OBLIGATIONS));
}
// Deleting an account that owns a claim id should fail
{
XEnv scEnv(*this, true);
scEnv.tx(create_bridge(Account::master, jvb))
.close()
.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
// We don't allow an account to be deleted if its sequence
// number is within 256 of the current ledger.
for (size_t i = 0; i < 256; ++i)
scEnv.close();
// try to delete scAlice, send funds to scBob
scEnv.tx(
acctdelete(scAlice, scBob),
fee(acctDelFee),
ter(tecHAS_OBLIGATIONS));
}
}
void
testBadPublicKey()
{
using namespace jtx;
testcase("Bad attestations");
{
// Create a bridge and add an attestation with a bad public key
XEnv scEnv(*this, true);
std::uint32_t const claimID = 1;
std::optional<Account> dst{scBob};
auto const amt = XRP(1000);
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close();
scEnv.tx(xchain_create_claim_id(scAlice, jvb, reward, mcAlice))
.close();
auto jvAtt = claim_attestation(
scAttester,
jvb,
mcAlice,
amt,
payees[UT_XCHAIN_DEFAULT_QUORUM],
true,
claimID,
dst,
signers[UT_XCHAIN_DEFAULT_QUORUM]);
{
// Change to an invalid keytype
auto k = jvAtt["PublicKey"].asString();
k.at(1) = '9';
jvAtt["PublicKey"] = k;
}
scEnv.tx(jvAtt, ter(temMALFORMED)).close();
}
{
// Create a bridge and add an create account attestation with a bad
// public key
XEnv scEnv(*this, true);
std::uint32_t const createCount = 1;
Account dst{scBob};
auto const amt = XRP(1000);
auto const rewardAmt = XRP(1);
scEnv.tx(create_bridge(Account::master, jvb))
.tx(jtx::signers(Account::master, quorum, signers))
.close();
auto jvAtt = create_account_attestation(
scAttester,
jvb,
mcAlice,
amt,
rewardAmt,
payees[UT_XCHAIN_DEFAULT_QUORUM],
true,
createCount,
dst,
signers[UT_XCHAIN_DEFAULT_QUORUM]);
{
// Change to an invalid keytype
auto k = jvAtt["PublicKey"].asString();
k.at(1) = '9';
jvAtt["PublicKey"] = k;
}
scEnv.tx(jvAtt, ter(temMALFORMED)).close();
}
}
void
run() override
{
testXChainBridgeExtraFields();
testXChainCreateBridge();
testXChainBridgeCreateConstraints();
testXChainCreateBridgeMatrix();
testXChainModifyBridge();
testXChainCreateClaimID();
testXChainCommit();
testXChainAddAttestation();
testXChainAddClaimNonBatchAttestation();
testXChainAddAccountCreateNonBatchAttestation();
testXChainClaim();
testXChainCreateAccount();
testFeeDipsIntoReserve();
testXChainDeleteDoor();
testBadPublicKey();
}
};
// -----------------------------------------------------------
// -----------------------------------------------------------
struct XChainSim_test : public beast::unit_test::suite,
public jtx::XChainBridgeObjects
{
private:
static constexpr size_t num_signers = 5;
// --------------------------------------------------
enum class WithClaim { no, yes };
struct Transfer
{
jtx::Account from;
jtx::Account to;
jtx::Account finaldest;
STAmount amt;
bool a2b; // direction of transfer
WithClaim with_claim{WithClaim::no};
uint32_t claim_id{0};
std::array<bool, num_signers> attested{};
};
struct AccountCreate
{
jtx::Account from;
jtx::Account to;
STAmount amt;
STAmount reward;
bool a2b;
uint32_t claim_id{0};
std::array<bool, num_signers> attested{};
};
using ENV = XEnv<XChainSim_test>;
using BridgeID = BridgeDef const*;
// tracking chain state
// --------------------
struct AccountStateTrack
{
STAmount startAmount{0};
STAmount expectedDiff{0};
void
init(ENV& env, jtx::Account const& acct)
{
startAmount = env.balance(acct);
expectedDiff = STAmount(0);
}
bool
verify(ENV& env, jtx::Account const& acct) const
{
STAmount diff{env.balance(acct) - startAmount};
bool check = diff == expectedDiff;
return check;
}
};
// --------------------------------------------------
struct ChainStateTrack
{
using ClaimVec = jtx::JValueVec;
using CreateClaimVec = jtx::JValueVec;
using CreateClaimMap = std::map<uint32_t, CreateClaimVec>;
ChainStateTrack(ENV& env)
: env(env), tx_fee(env.env_.current()->fees().base)
{
}
void
sendAttestations(size_t signer_idx, BridgeID bridge, ClaimVec& claims)
{
for (auto const& c : claims)
{
env.tx(c).close();
spendFee(bridge->signers[signer_idx].account);
}
claims.clear();
}
uint32_t
sendCreateAttestations(
size_t signer_idx,
BridgeID bridge,
CreateClaimVec& claims)
{
size_t num_successful = 0;
for (auto const& c : claims)
{
env.tx(c).close();
if (env.ter() == tesSUCCESS)
{
counters[bridge].signers.push_back(signer_idx);
num_successful++;
}
spendFee(bridge->signers[signer_idx].account);
}
claims.clear();
return num_successful;
}
void
sendAttestations()
{
bool callback_called;
// we have this "do {} while" loop because we want to process
// all the account create which can reach quorum at this time
// stamp.
do
{
callback_called = false;
for (size_t i = 0; i < signers_attns.size(); ++i)
{
for (auto& [bridge, claims] : signers_attns[i])
{
sendAttestations(i, bridge, claims.xfer_claims);
auto& c = counters[bridge];
auto& create_claims =
claims.create_claims[c.claim_count];
auto num_attns = create_claims.size();
if (num_attns)
{
c.num_create_attn_sent += sendCreateAttestations(
i, bridge, create_claims);
}
assert(claims.create_claims[c.claim_count].empty());
}
}
for (auto& [bridge, c] : counters)
{
if (c.num_create_attn_sent >= bridge->quorum)
{
callback_called = true;
c.create_callbacks[c.claim_count](c.signers);
++c.claim_count;
c.num_create_attn_sent = 0;
c.signers.clear();
}
}
} while (callback_called);
}
void
init(jtx::Account const& acct)
{
accounts[acct].init(env, acct);
}
void
receive(
jtx::Account const& acct,
STAmount amt,
std::uint64_t divisor = 1)
{
if (amt.issue() != xrpIssue())
return;
auto it = accounts.find(acct);
if (it == accounts.end())
{
accounts[acct].init(env, acct);
// we just looked up the account, so expectedDiff == 0
}
else
{
it->second.expectedDiff +=
(divisor == 1 ? amt
: divide(
amt,
STAmount(amt.issue(), divisor),
amt.issue()));
}
}
void
spend(jtx::Account const& acct, STAmount amt, std::uint64_t times = 1)
{
if (amt.issue() != xrpIssue())
return;
receive(
acct,
times == 1
? -amt
: -multiply(
amt, STAmount(amt.issue(), times), amt.issue()));
}
void
transfer(jtx::Account const& from, jtx::Account const& to, STAmount amt)
{
spend(from, amt);
receive(to, amt);
}
void
spendFee(jtx::Account const& acct, size_t times = 1)
{
spend(acct, tx_fee, times);
}
bool
verify() const
{
for (auto const& [acct, state] : accounts)
if (!state.verify(env, acct))
return false;
return true;
}
struct BridgeCounters
{
using complete_cb =
std::function<void(std::vector<size_t> const& signers)>;
uint32_t claim_id{0};
uint32_t create_count{0}; // for account create. First should be 1
uint32_t claim_count{
0}; // for account create. Increments after quorum for
// current create_count (starts at 1) is reached.
uint32_t num_create_attn_sent{0}; // for current claim_count
std::vector<size_t> signers;
std::vector<complete_cb> create_callbacks;
};
struct Claims
{
ClaimVec xfer_claims;
CreateClaimMap create_claims;
};
using SignerAttns = std::unordered_map<BridgeID, Claims>;
using SignersAttns = std::array<SignerAttns, num_signers>;
ENV& env;
std::map<jtx::Account, AccountStateTrack> accounts;
SignersAttns signers_attns;
std::map<BridgeID, BridgeCounters> counters;
STAmount tx_fee;
};
struct ChainStateTracker
{
ChainStateTracker(ENV& a_env, ENV& b_env) : a_(a_env), b_(b_env)
{
}
bool
verify() const
{
return a_.verify() && b_.verify();
}
void
sendAttestations()
{
a_.sendAttestations();
b_.sendAttestations();
}
void
init(jtx::Account const& acct)
{
a_.init(acct);
b_.init(acct);
}
ChainStateTrack a_;
ChainStateTrack b_;
};
enum SmState {
st_initial,
st_claimid_created,
st_attesting,
st_attested,
st_completed,
st_closed,
};
enum Act_Flags { af_a2b = 1 << 0 };
// --------------------------------------------------
template <class T>
class SmBase
{
public:
SmBase(
std::shared_ptr<ChainStateTracker> const& chainstate,
BridgeDef const& bridge)
: bridge_(bridge), st_(chainstate)
{
}
ChainStateTrack&
srcState()
{
return static_cast<T&>(*this).a2b() ? st_->a_ : st_->b_;
}
ChainStateTrack&
destState()
{
return static_cast<T&>(*this).a2b() ? st_->b_ : st_->a_;
}
jtx::Account const&
srcDoor()
{
return static_cast<T&>(*this).a2b() ? bridge_.doorA : bridge_.doorB;
}
jtx::Account const&
dstDoor()
{
return static_cast<T&>(*this).a2b() ? bridge_.doorB : bridge_.doorA;
}
protected:
BridgeDef const& bridge_;
std::shared_ptr<ChainStateTracker> st_;
};
// --------------------------------------------------
class SmCreateAccount : public SmBase<SmCreateAccount>
{
public:
using Base = SmBase<SmCreateAccount>;
SmCreateAccount(
std::shared_ptr<ChainStateTracker> const& chainstate,
BridgeDef const& bridge,
AccountCreate create)
: Base(chainstate, bridge)
, sm_state(st_initial)
, cr(std::move(create))
{
}
bool
a2b() const
{
return cr.a2b;
}
uint32_t
issue_account_create()
{
ChainStateTrack& st = srcState();
jtx::Account const& srcdoor = srcDoor();
st.env
.tx(sidechain_xchain_account_create(
cr.from, bridge_.jvb, cr.to, cr.amt, cr.reward))
.close(); // needed for claim_id sequence to be correct'
st.spendFee(cr.from);
st.transfer(cr.from, srcdoor, cr.amt);
st.transfer(cr.from, srcdoor, cr.reward);
return ++st.counters[&bridge_].create_count;
}
void
attest(uint64_t time, uint32_t rnd)
{
ChainStateTrack& st = destState();
// check all signers, but start at a random one
size_t i;
for (i = 0; i < num_signers; ++i)
{
size_t signer_idx = (rnd + i) % num_signers;
if (!(cr.attested[signer_idx]))
{
// enqueue one attestation for this signer
cr.attested[signer_idx] = true;
st.signers_attns[signer_idx][&bridge_]
.create_claims[cr.claim_id - 1]
.emplace_back(create_account_attestation(
bridge_.signers[signer_idx].account,
bridge_.jvb,
cr.from,
cr.amt,
cr.reward,
bridge_.signers[signer_idx].account,
cr.a2b,
cr.claim_id,
cr.to,
bridge_.signers[signer_idx]));
break;
}
}
if (i == num_signers)
return; // did not attest
auto& counters = st.counters[&bridge_];
if (counters.create_callbacks.size() < cr.claim_id)
counters.create_callbacks.resize(cr.claim_id);
auto complete_cb = [&](std::vector<size_t> const& signers) {
auto num_attestors = signers.size();
st.env.close();
assert(
num_attestors <=
std::count(cr.attested.begin(), cr.attested.end(), true));
assert(num_attestors >= bridge_.quorum);
assert(cr.claim_id - 1 == counters.claim_count);
auto r = cr.reward;
auto reward = divide(r, STAmount(num_attestors), r.issue());
for (auto i : signers)
st.receive(bridge_.signers[i].account, reward);
st.spend(dstDoor(), reward, num_attestors);
st.transfer(dstDoor(), cr.to, cr.amt);
st.env.env_.memoize(cr.to);
sm_state = st_completed;
};
counters.create_callbacks[cr.claim_id - 1] = std::move(complete_cb);
}
SmState
advance(uint64_t time, uint32_t rnd)
{
switch (sm_state)
{
case st_initial:
cr.claim_id = issue_account_create();
sm_state = st_attesting;
break;
case st_attesting:
attest(time, rnd);
break;
default:
assert(0);
break;
case st_completed:
break; // will get this once
}
return sm_state;
}
private:
SmState sm_state;
AccountCreate cr;
};
// --------------------------------------------------
class SmTransfer : public SmBase<SmTransfer>
{
public:
using Base = SmBase<SmTransfer>;
SmTransfer(
std::shared_ptr<ChainStateTracker> const& chainstate,
BridgeDef const& bridge,
Transfer xfer)
: Base(chainstate, bridge)
, xfer(std::move(xfer))
, sm_state(st_initial)
{
}
bool
a2b() const
{
return xfer.a2b;
}
uint32_t
create_claim_id()
{
ChainStateTrack& st = destState();
st.env
.tx(xchain_create_claim_id(
xfer.to, bridge_.jvb, bridge_.reward, xfer.from))
.close(); // needed for claim_id sequence to be
// correct'
st.spendFee(xfer.to);
return ++st.counters[&bridge_].claim_id;
}
void
commit()
{
ChainStateTrack& st = srcState();
jtx::Account const& srcdoor = srcDoor();
if (xfer.amt.issue() != xrpIssue())
{
st.env.tx(pay(srcdoor, xfer.from, xfer.amt));
st.spendFee(srcdoor);
}
st.env.tx(xchain_commit(
xfer.from,
bridge_.jvb,
xfer.claim_id,
xfer.amt,
xfer.with_claim == WithClaim::yes
? std::nullopt
: std::optional<jtx::Account>(xfer.finaldest)));
st.spendFee(xfer.from);
st.transfer(xfer.from, srcdoor, xfer.amt);
}
void
distribute_reward(ChainStateTrack& st)
{
auto r = bridge_.reward;
auto reward = divide(r, STAmount(bridge_.quorum), r.issue());
for (size_t i = 0; i < num_signers; ++i)
{
if (xfer.attested[i])
st.receive(bridge_.signers[i].account, reward);
}
st.spend(xfer.to, reward, bridge_.quorum);
}
bool
attest(uint64_t time, uint32_t rnd)
{
ChainStateTrack& st = destState();
// check all signers, but start at a random one
for (size_t i = 0; i < num_signers; ++i)
{
size_t signer_idx = (rnd + i) % num_signers;
if (!(xfer.attested[signer_idx]))
{
// enqueue one attestation for this signer
xfer.attested[signer_idx] = true;
st.signers_attns[signer_idx][&bridge_]
.xfer_claims.emplace_back(claim_attestation(
bridge_.signers[signer_idx].account,
bridge_.jvb,
xfer.from,
xfer.amt,
bridge_.signers[signer_idx].account,
xfer.a2b,
xfer.claim_id,
xfer.with_claim == WithClaim::yes
? std::nullopt
: std::optional<jtx::Account>(xfer.finaldest),
bridge_.signers[signer_idx]));
break;
}
}
// return true if quorum was reached, false otherwise
bool quorum =
std::count(xfer.attested.begin(), xfer.attested.end(), true) >=
bridge_.quorum;
if (quorum && xfer.with_claim == WithClaim::no)
{
distribute_reward(st);
st.transfer(dstDoor(), xfer.finaldest, xfer.amt);
}
return quorum;
}
void
claim()
{
ChainStateTrack& st = destState();
st.env.tx(xchain_claim(
xfer.to, bridge_.jvb, xfer.claim_id, xfer.amt, xfer.finaldest));
distribute_reward(st);
st.transfer(dstDoor(), xfer.finaldest, xfer.amt);
st.spendFee(xfer.to);
}
SmState
advance(uint64_t time, uint32_t rnd)
{
switch (sm_state)
{
case st_initial:
xfer.claim_id = create_claim_id();
sm_state = st_claimid_created;
break;
case st_claimid_created:
commit();
sm_state = st_attesting;
break;
case st_attesting:
sm_state = attest(time, rnd)
? (xfer.with_claim == WithClaim::yes ? st_attested
: st_completed)
: st_attesting;
break;
case st_attested:
assert(xfer.with_claim == WithClaim::yes);
claim();
sm_state = st_completed;
break;
default:
case st_completed:
assert(0); // should have been removed
break;
}
return sm_state;
}
private:
Transfer xfer;
SmState sm_state;
};
// --------------------------------------------------
using Sm = std::variant<SmCreateAccount, SmTransfer>;
using SmCont = std::list<std::pair<uint64_t, Sm>>;
SmCont sm_;
void
xfer(
uint64_t time,
std::shared_ptr<ChainStateTracker> const& chainstate,
BridgeDef const& bridge,
Transfer transfer)
{
sm_.emplace_back(
time, SmTransfer(chainstate, bridge, std::move(transfer)));
}
void
ac(uint64_t time,
std::shared_ptr<ChainStateTracker> const& chainstate,
BridgeDef const& bridge,
AccountCreate ac)
{
sm_.emplace_back(
time, SmCreateAccount(chainstate, bridge, std::move(ac)));
}
public:
void
runSimulation(
std::shared_ptr<ChainStateTracker> const& st,
bool verify_balances = true)
{
using namespace jtx;
uint64_t time = 0;
std::mt19937 gen(27); // Standard mersenne_twister_engine
std::uniform_int_distribution<uint32_t> distrib(0, 9);
while (!sm_.empty())
{
++time;
for (auto it = sm_.begin(); it != sm_.end();)
{
auto vis = [&](auto& sm) {
uint32_t rnd = distrib(gen);
return sm.advance(time, rnd);
};
auto& [t, sm] = *it;
if (t <= time && std::visit(vis, sm) == st_completed)
it = sm_.erase(it);
else
++it;
}
// send attestations
st->sendAttestations();
// make sure all transactions have been applied
st->a_.env.close();
st->b_.env.close();
if (verify_balances)
{
BEAST_EXPECT(st->verify());
}
}
}
void
testXChainSimulation()
{
using namespace jtx;
testcase("Bridge usage simulation");
XEnv mcEnv(*this);
XEnv scEnv(*this, true);
auto st = std::make_shared<ChainStateTracker>(mcEnv, scEnv);
// create 10 accounts + door funded on both chains, and store
// in ChainStateTracker the initial amount of these accounts
Account doorXRPLocking("doorXRPLocking"),
doorUSDLocking("doorUSDLocking"), doorUSDIssuing("doorUSDIssuing");
constexpr size_t num_acct = 10;
auto a = [&doorXRPLocking, &doorUSDLocking, &doorUSDIssuing]() {
using namespace std::literals;
std::vector<Account> result;
result.reserve(num_acct);
for (int i = 0; i < num_acct; ++i)
result.emplace_back(
"a"s + std::to_string(i),
(i % 2) ? KeyType::ed25519 : KeyType::secp256k1);
result.emplace_back("doorXRPLocking");
doorXRPLocking = result.back();
result.emplace_back("doorUSDLocking");
doorUSDLocking = result.back();
result.emplace_back("doorUSDIssuing");
doorUSDIssuing = result.back();
return result;
}();
for (auto& acct : a)
{
STAmount amt{XRP(100000)};
mcEnv.fund(amt, acct);
scEnv.fund(amt, acct);
}
Account USDLocking{"USDLocking"};
IOU usdLocking{USDLocking["USD"]};
IOU usdIssuing{doorUSDIssuing["USD"]};
mcEnv.fund(XRP(100000), USDLocking);
mcEnv.close();
mcEnv.tx(trust(doorUSDLocking, usdLocking(100000)));
mcEnv.close();
mcEnv.tx(pay(USDLocking, doorUSDLocking, usdLocking(50000)));
for (int i = 0; i < a.size(); ++i)
{
auto& acct{a[i]};
if (i < num_acct)
{
mcEnv.tx(trust(acct, usdLocking(100000)));
scEnv.tx(trust(acct, usdIssuing(100000)));
}
st->init(acct);
}
for (auto& s : signers)
st->init(s.account);
st->b_.init(Account::master);
// also create some unfunded accounts
constexpr size_t num_ua = 20;
auto ua = []() {
using namespace std::literals;
std::vector<Account> result;
result.reserve(num_ua);
for (int i = 0; i < num_ua; ++i)
result.emplace_back(
"ua"s + std::to_string(i),
(i % 2) ? KeyType::ed25519 : KeyType::secp256k1);
return result;
}();
// initialize a bridge from a BridgeDef
auto initBridge = [&mcEnv, &scEnv, &st](BridgeDef& bd) {
bd.initBridge(mcEnv, scEnv);
st->a_.spendFee(bd.doorA, 2);
st->b_.spendFee(bd.doorB, 2);
};
// create XRP -> XRP bridge
// ------------------------
BridgeDef xrp_b{
doorXRPLocking,
xrpIssue(),
Account::master,
xrpIssue(),
XRP(1),
XRP(20),
quorum,
signers,
Json::nullValue};
initBridge(xrp_b);
// create USD -> USD bridge
// ------------------------
BridgeDef usd_b{
doorUSDLocking,
usdLocking,
doorUSDIssuing,
usdIssuing,
XRP(1),
XRP(20),
quorum,
signers,
Json::nullValue};
initBridge(usd_b);
// try a single account create + transfer to validate the simulation
// engine. Do the transfer 8 time steps after the account create, to
// give time enough for ua[0] to be funded now so it can reserve
// the claimID
// -----------------------------------------------------------------
ac(0, st, xrp_b, {a[0], ua[0], XRP(777), xrp_b.reward, true});
xfer(8, st, xrp_b, {a[0], ua[0], a[2], XRP(3), true});
runSimulation(st);
// try the same thing in the other direction
// -----------------------------------------
ac(0, st, xrp_b, {a[0], ua[0], XRP(777), xrp_b.reward, false});
xfer(8, st, xrp_b, {a[0], ua[0], a[2], XRP(3), false});
runSimulation(st);
// run multiple XRP transfers
// --------------------------
xfer(0, st, xrp_b, {a[0], a[0], a[1], XRP(6), true, WithClaim::no});
xfer(1, st, xrp_b, {a[0], a[0], a[1], XRP(8), false, WithClaim::no});
xfer(1, st, xrp_b, {a[1], a[1], a[1], XRP(1), true});
xfer(2, st, xrp_b, {a[0], a[0], a[1], XRP(3), false});
xfer(2, st, xrp_b, {a[1], a[1], a[1], XRP(5), false});
xfer(2, st, xrp_b, {a[0], a[0], a[1], XRP(7), false, WithClaim::no});
xfer(2, st, xrp_b, {a[1], a[1], a[1], XRP(9), true});
runSimulation(st);
// run one USD transfer
// --------------------
xfer(0, st, usd_b, {a[0], a[1], a[2], usdLocking(3), true});
runSimulation(st);
// run multiple USD transfers
// --------------------------
xfer(0, st, usd_b, {a[0], a[0], a[1], usdLocking(6), true});
xfer(1, st, usd_b, {a[0], a[0], a[1], usdIssuing(8), false});
xfer(1, st, usd_b, {a[1], a[1], a[1], usdLocking(1), true});
xfer(2, st, usd_b, {a[0], a[0], a[1], usdIssuing(3), false});
xfer(2, st, usd_b, {a[1], a[1], a[1], usdIssuing(5), false});
xfer(2, st, usd_b, {a[0], a[0], a[1], usdIssuing(7), false});
xfer(2, st, usd_b, {a[1], a[1], a[1], usdLocking(9), true});
runSimulation(st);
// run mixed transfers
// -------------------
xfer(0, st, xrp_b, {a[0], a[0], a[0], XRP(1), true});
xfer(0, st, usd_b, {a[1], a[3], a[3], usdIssuing(3), false});
xfer(0, st, usd_b, {a[3], a[2], a[1], usdIssuing(5), false});
xfer(1, st, xrp_b, {a[0], a[0], a[0], XRP(4), false});
xfer(1, st, xrp_b, {a[1], a[1], a[0], XRP(8), true});
xfer(1, st, usd_b, {a[4], a[1], a[1], usdLocking(7), true});
xfer(3, st, xrp_b, {a[1], a[1], a[0], XRP(7), true});
xfer(3, st, xrp_b, {a[0], a[4], a[3], XRP(2), false});
xfer(3, st, xrp_b, {a[1], a[1], a[0], XRP(9), true});
xfer(3, st, usd_b, {a[3], a[1], a[1], usdIssuing(11), false});
runSimulation(st);
// run multiple account create to stress attestation batching
// ----------------------------------------------------------
ac(0, st, xrp_b, {a[0], ua[1], XRP(301), xrp_b.reward, true});
ac(0, st, xrp_b, {a[1], ua[2], XRP(302), xrp_b.reward, true});
ac(1, st, xrp_b, {a[0], ua[3], XRP(303), xrp_b.reward, true});
ac(2, st, xrp_b, {a[1], ua[4], XRP(304), xrp_b.reward, true});
ac(3, st, xrp_b, {a[0], ua[5], XRP(305), xrp_b.reward, true});
ac(4, st, xrp_b, {a[1], ua[6], XRP(306), xrp_b.reward, true});
ac(6, st, xrp_b, {a[0], ua[7], XRP(307), xrp_b.reward, true});
ac(7, st, xrp_b, {a[2], ua[8], XRP(308), xrp_b.reward, true});
ac(9, st, xrp_b, {a[0], ua[9], XRP(309), xrp_b.reward, true});
ac(9, st, xrp_b, {a[0], ua[9], XRP(309), xrp_b.reward, true});
ac(10, st, xrp_b, {a[0], ua[10], XRP(310), xrp_b.reward, true});
ac(12, st, xrp_b, {a[0], ua[11], XRP(311), xrp_b.reward, true});
ac(12, st, xrp_b, {a[3], ua[12], XRP(312), xrp_b.reward, true});
ac(12, st, xrp_b, {a[4], ua[13], XRP(313), xrp_b.reward, true});
ac(12, st, xrp_b, {a[3], ua[14], XRP(314), xrp_b.reward, true});
ac(12, st, xrp_b, {a[6], ua[15], XRP(315), xrp_b.reward, true});
ac(13, st, xrp_b, {a[7], ua[16], XRP(316), xrp_b.reward, true});
ac(15, st, xrp_b, {a[3], ua[17], XRP(317), xrp_b.reward, true});
runSimulation(st, true); // balances verification working now.
}
void
run() override
{
testXChainSimulation();
}
};
BEAST_DEFINE_TESTSUITE(XChain, app, ripple);
BEAST_DEFINE_TESTSUITE(XChainSim, app, ripple);
} // namespace ripple::test