mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 16:56:48 +00:00
This change replaces all instances of `<variable> != tesSUCCESS` with `!isTesSuccess(<variable>)` and `<variable> == tesSUCCESS` with `isTesSuccess(<variable>)`.
4595 lines
165 KiB
C++
4595 lines
165 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 xrpl::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 const& 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 payer_; // 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& payer,
|
|
jtx::Account const* payees,
|
|
size_t num_payees,
|
|
bool withClaim)
|
|
: from_(env, from_acct)
|
|
, to_(env, to_acct)
|
|
, payer_(env, payer)
|
|
, 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& payer,
|
|
std::vector<jtx::Account> const& payees,
|
|
bool withClaim)
|
|
: BalanceTransfer(env, from_acct, to_acct, payer, &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 || payer_.diff() == -(reward_cost + txFees_));
|
|
}
|
|
|
|
bool
|
|
has_not_happened()
|
|
{
|
|
return check_most_balances(STAmount(0), STAmount(0)) &&
|
|
payer_.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 bridges
|
|
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 = isTesSuccess(mcTER) && isTesSuccess(scTER);
|
|
|
|
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 reach 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() // cspell: disable-line
|
|
{
|
|
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 payer 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 payer 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(); // cspell: disable-line
|
|
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 = false;
|
|
|
|
// 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;
|
|
// cspell: ignore attns
|
|
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_claim_id_created,
|
|
st_attesting,
|
|
st_attested,
|
|
st_completed,
|
|
st_closed,
|
|
};
|
|
|
|
enum Act_Flags { af_a2b = 1 << 0 };
|
|
|
|
// --------------------------------------------------
|
|
template <class T>
|
|
class SmBase
|
|
{
|
|
SmBase(std::shared_ptr<ChainStateTracker> const& chainstate, BridgeDef const& bridge)
|
|
: bridge_(bridge), st_(chainstate)
|
|
{
|
|
}
|
|
|
|
public:
|
|
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_;
|
|
|
|
friend T;
|
|
};
|
|
|
|
// --------------------------------------------------
|
|
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), 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 = 0;
|
|
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{st_initial};
|
|
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))
|
|
{
|
|
}
|
|
|
|
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_claim_id_created;
|
|
break;
|
|
|
|
case st_claim_id_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{st_initial};
|
|
};
|
|
|
|
// --------------------------------------------------
|
|
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, xrpl);
|
|
BEAST_DEFINE_TESTSUITE(XChainSim, app, xrpl);
|
|
|
|
} // namespace xrpl::test
|