#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 struct SEnv { jtx::Env env; SEnv( T& s, std::unique_ptr config, FeatureBitset features, std::unique_ptr logs = nullptr, beast::Severity thresh = beast::Severity::Error) : 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 SEnv& fund(STAmount const& amount, Arg const& arg, Args const&... args) { env.fund(amount, arg, args...); return *this; } template SEnv& tx(JsonValue&& jv, FN const&... fN) { env(std::forward(jv), fN...); return *this; } template SEnv& multiTx(jtx::JValueVec const& jvv, FN const&... fN) { for (auto const& jv : jvv) env(jv, fN...); return *this; } [[nodiscard]] TER ter() const { return env.ter(); } [[nodiscard]] STAmount balance(jtx::Account const& account) const { return env.balance(account).value(); } [[nodiscard]] 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 account(jtx::Account const& account) { return env.le(account); } std::shared_ptr bridge(json::Value const& jvb) { STXChainBridge const b(jvb); auto tryGet = [&](STXChainBridge::ChainType ct) -> std::shared_ptr { 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 claimID(json::Value const& jvb, std::uint64_t seq) { return env.le(keylet::xChainClaimID(STXChainBridge(jvb), seq)); } std::shared_ptr 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 is that it // funds some default accounts, and that it enables `testable_amendments() | // FeatureBitset{featureXChainBridge}` by default. // ----------------------------------------------------------------------------- template struct XEnv : public jtx::XChainBridgeObjects, public SEnv { XEnv(T& s, bool side = false) : SEnv(s, jtx::envconfig(), features) { using namespace jtx; STAmount const xrpFunds{XRP(10000)}; if (!side) { this->fund(xrpFunds, 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(xrpFunds, s.account); } else { this->fund(xrpFunds, scDoor, scAlice, scBob, scCarol, scGw, scAttester, scReward); for (auto& ra : payees) this->fund(xrpFunds, ra); for (auto& s : signers) this->fund(xrpFunds, s.account); // Signer's list must match the attestation signers // env_(jtx::signers(Account::kMaster, quorum, signers)); } this->close(); } }; // Tracks the xrp balance for one account template 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); } [[nodiscard]] STAmount diff() const { return env.balance(account) - startAmount; } }; // Tracks the xrp balance for multiple accounts involved in a crosss-chain // transfer template struct BalanceTransfer { using balance = Balance; balance from; balance to; balance payer; // pays the rewards std::vector rewardAccounts; // receives the reward XRPAmount txFees; BalanceTransfer( T& env, jtx::Account const& fromAcct, jtx::Account const& toAcct, jtx::Account const& payer, jtx::Account const* payees, size_t numPayees, bool withClaim) : from(env, fromAcct) , to(env, toAcct) , payer(env, payer) , rewardAccounts([&]() { std::vector r; r.reserve(numPayees); for (size_t i = 0; i < numPayees; ++i) r.emplace_back(env, payees[i]); return r; }()) , txFees(withClaim ? env.env.current()->fees().base : XRPAmount(0)) { } BalanceTransfer( T& env, jtx::Account const& fromAcct, jtx::Account const& toAcct, jtx::Account const& payer, std::vector const& payees, bool withClaim) : BalanceTransfer(env, fromAcct, toAcct, payer, &payees[0], payees.size(), withClaim) { } [[nodiscard]] bool payeesReceived(STAmount const& reward) const { return std::all_of(rewardAccounts.begin(), rewardAccounts.end(), [&](balance const& b) { return b.diff() == reward; }); } bool checkMostBalances(STAmount const& amt, STAmount const& reward) { return from.diff() == -amt && to.diff() == amt && payeesReceived(reward); } bool hasHappened(STAmount const& amt, STAmount const& reward, bool checkPayer = true) { auto rewardCost = multiply(reward, STAmount(rewardAccounts.size()), reward.asset()); return checkMostBalances(amt, reward) && (!checkPayer || payer.diff() == -(rewardCost + txFees)); } bool hasNotHappened() { return checkMostBalances(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 const& signers; json::Value jvb; template void initBridge(ENV& mcEnv, ENV& scEnv) { jvb = bridge(doorA, issueA, doorB, issueB); auto const optAccountCreate = [&]() -> std::optional { if (issueA != xrpIssue() || issueB != xrpIssue()) return {}; return minAccountCreate; }(); mcEnv.tx(bridgeCreate(doorA, jvb, reward, optAccountCreate)) .tx(jtx::signers(doorA, quorum, signers)) .close(); scEnv.tx(bridgeCreate(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 = createBridge(mcDoor)[sfXChainBridge.jsonName]; bool exceptionPresent = false; try { exceptionPresent = false; [[maybe_unused]] STXChainBridge const testBridge1(jBridge); } catch (std::exception& ec) { exceptionPresent = true; } BEAST_EXPECT(!exceptionPresent); try { exceptionPresent = false; jBridge["Extra"] = 1; [[maybe_unused]] STXChainBridge const testBridge2(jBridge); } catch ([[maybe_unused]] std::exception& ec) { exceptionPresent = true; } BEAST_EXPECT(exceptionPresent); } void testXChainCreateBridge() { XRPAmount const res1 = reserve(1); using namespace jtx; testcase("Create Bridge"); // Normal create_bridge => should succeed XEnv(*this).tx(createBridge(mcDoor)).close(); // Bridge not owned by one of the door account. XEnv(*this).tx(createBridge(mcBob), Ter(temXCHAIN_BRIDGE_NONDOOR_OWNER)); // Create twice on the same account XEnv(*this).tx(createBridge(mcDoor)).close().tx(createBridge(mcDoor), Ter(tecDUPLICATE)); // Create USD bridge Alice -> Bob ... should succeed XEnv(*this).tx( createBridge(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( createBridge(mcAlice, bridge(mcAlice, mcAlice["USD"], mcBob, mcBob["USD"])), Ter(temXCHAIN_BRIDGE_BAD_ISSUES)); // Bridge where the two door accounts are equal. XEnv(*this).tx( createBridge(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(createBridge(mcAlice, bridge(mcAlice, mcGw["USD"], mcBob, mcBob["USD"]))) .close() .tx(createBridge(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(createBridge(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(createBridge(mcuDoor, jvub), Ter(tecINSUFFICIENT_RESERVE)); // Reward amount is non-xrp XEnv(*this).tx( createBridge(mcDoor, jvb, mcUSD(1)), Ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT)); // Reward amount is XRP and negative XEnv(*this).tx(createBridge(mcDoor, jvb, XRP(-1)), Ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT)); // Reward amount is 1 xrp => should succeed XEnv(*this).tx(createBridge(mcDoor, jvb, XRP(1)), Ter(tesSUCCESS)); // Min create amount is 1 xrp, mincreate is 1 xrp => should succeed XEnv(*this).tx(createBridge(mcDoor, jvb, XRP(1), XRP(1)), Ter(tesSUCCESS)); // Min create amount is non-xrp XEnv(*this).tx( createBridge(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( createBridge(mcDoor, jvb, XRP(1), XRP(0)), Ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT)); // Min create amount is negative XEnv(*this).tx( createBridge(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, testableAmendments() - featureXChainBridge); env(createBridge(Account::kMaster, jvb), Ter(temDISABLED)); } // coverage test: BridgeCreate::preclaim() returns tecNO_ISSUER. XEnv(*this).tx( createBridge(mcAlice, bridge(mcAlice, mcuAlice["USD"], mcBob, mcBob["USD"])), Ter(tecNO_ISSUER)); // coverage test: create_bridge transaction with incorrect flag XEnv(*this).tx(createBridge(mcAlice, jvb), Txflags(tfFillOrKill), Ter(temINVALID_FLAG)); // coverage test: create_bridge transaction with xchain feature disabled XEnv(*this) .disableFeature(featureXChainBridge) .tx(createBridge(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(createBridge(a1, bridge(a1, gusd, b, busd))).close(); env.tx(createBridge(a2, bridge(a2, gusd, b, busd))).close(); // Add the exact same bridge to two different accounts (one locking // account and one issuing) env.tx(createBridge(a3, bridge(a3, gusd, a4, a4["USD"]))).close(); env.tx(createBridge(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(createBridge(a5, bridge(a6, gusd, a5, a5["USD"]))).close(); env.tx(createBridge(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(createBridge(b, goodBridge1)).close(); // Issuing asset is the same, this is a duplicate env.tx(createBridge(b, bridge(a, geur, b, busd)), Ter(tecDUPLICATE)).close(); env.tx(createBridge(a, goodBridge2), Ter(tesSUCCESS)).close(); // Locking asset is the same - this is a duplicate env.tx(createBridge(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(createBridge(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(xchainCommit(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(xchainCommit(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(bridgeModify(b, goodBridge1, XRP(33), std::nullopt)).close(); BEAST_EXPECT((*env.bridge(goodBridge1))[sfSignatureReward] == XRP(33)); env.tx(bridgeModify(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::kMaster; ib = xrpIssue(); })}; std::vector> expectedResult{ {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> testResult; 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 = expectedResult[testResult.size()]; mcEnv.tx(createBridge(a, bridge(a, ia, b, ib)), Ter(TER::fromInt(expected.first))); TER const mcTER = mcEnv.env.ter(); scEnv.tx(createBridge(b, bridge(a, ia, b, ib)), Ter(TER::fromInt(expected.second))); TER const scTER = scEnv.env.ter(); bool const pass = isTesSuccess(mcTER) && isTesSuccess(scTER); testResult.emplace_back(mcTER, scTER, pass); }; auto applyIcs = [&](auto const& lc, auto const& ics) { std::apply([&](auto const&... ic) { (testcase(lc, ic), ...); }, ics); }; std::apply([&](auto const&... lc) { (applyIcs(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( bridgeModify( 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(createBridge(mcDoor, jvb, XRP(1), XRP(1))) .close() .tx(bridgeModify(mcDoor, jvb, {}, {}), Ter(temMALFORMED)); // Reward amount is non-xrp XEnv(*this).tx( bridgeModify(mcDoor, jvb, mcUSD(2), XRP(10)), Ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT)); // Reward amount is XRP and negative XEnv(*this).tx( bridgeModify(mcDoor, jvb, XRP(-2), XRP(10)), Ter(temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT)); // Min create amount is non-xrp XEnv(*this).tx( bridgeModify(mcDoor, jvb, XRP(2), mcUSD(10)), Ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT)); // Min create amount is zero XEnv(*this).tx( bridgeModify(mcDoor, jvb, XRP(2), XRP(0)), Ter(temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT)); // Min create amount is negative XEnv(*this).tx( bridgeModify(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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scBob)).close(); } BEAST_EXPECT(transfer.hasHappened(amt, splitRewardQuorum)); } // 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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); // Now modify the reward on the bridge mcEnv.tx(bridgeModify(mcDoor, jvb, XRP(2), XRP(10))).close(); scEnv.tx(bridgeModify(Account::kMaster, jvb, XRP(2), XRP(10))).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(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.hasHappened(amt, splitRewardQuorum)); } // 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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(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::kMaster, quorum, altSigners)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); // submit claim using outdated signers - should fail scEnv .multiTx( claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers), Ter(tecNO_PERMISSION)) .close(); if (withClaim) { // need to submit a claim transactions scEnv .tx(xchainClaim(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.hasNotHappened()); // submit claim using current signers - should succeed scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, altSigners)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scBob)).close(); } // make sure the transfer went through as we sent attestations // using new signers BEAST_EXPECT(transfer.hasHappened(amt, splitRewardQuorum, false)); } // coverage test: bridge_modify transaction with incorrect flag XEnv(*this) .tx(createBridge(mcDoor, jvb)) .close() .tx(bridgeModify(mcDoor, jvb, XRP(1), XRP(2)), Txflags(tfFillOrKill), Ter(temINVALID_FLAG)); // coverage test: bridge_modify transaction with xchain feature // disabled XEnv(*this) .tx(createBridge(mcDoor, jvb)) .disableFeature(featureXChainBridge) .close() .tx(bridgeModify(mcDoor, jvb, XRP(1), XRP(2)), Ter(temDISABLED)); // coverage test: bridge_modify return temSIDECHAIN_NONDOOR_OWNER; XEnv(*this) .tx(createBridge(mcDoor, jvb)) .close() .tx(bridgeModify(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(createBridge(mcDoor, jvb, XRP(1), XRP(20))) .close() .tx(sidechainXchainAccountCreate(mcAlice, jvb, scuAlice, XRP(100), reward)) .close() .tx(bridgeModify(mcDoor, jvb, {}, XRP(2)), Txflags(tfClearAccountCreateAmount), Ter(temMALFORMED)) .close() .tx(bridgeModify(mcDoor, jvb, XRP(3), {}), Txflags(tfClearAccountCreateAmount)) .close() .tx(sidechainXchainAccountCreate(mcAlice, jvb, scuBob, XRP(100), XRP(3)), Ter(tecXCHAIN_CREATE_ACCOUNT_DISABLED)) .close(); } void testXChainCreateClaimID() { using namespace jtx; XRPAmount const res1 = reserve(1); XRPAmount const fee = txFee(); testcase("Create ClaimID"); // normal bridge create for sanity check with the exact necessary // account balance XEnv(*this, true) .tx(createBridge(Account::kMaster, jvb)) .fund(res1, scuAlice) // acct reserve + 1 object .close() .tx(xchainCreateClaimId(scuAlice, jvb, reward, mcAlice)) .close(); // check reward not deducted when claim id is created { XEnv xenv(*this, true); test::Balance const scAliceBal(xenv, scAlice); xenv.tx(createBridge(Account::kMaster, jvb)) .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); BEAST_EXPECT(scAliceBal.diff() == -fee); } // Non-existent bridge XEnv(*this, true) .tx(xchainCreateClaimId( 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(createBridge(Account::kMaster, jvb)) .fund(res1 - xrpDust, scuAlice) // barely not enough .close() .tx(xchainCreateClaimId(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(createBridge(Account::kMaster, jvb)) .close() .tx(xchainCreateClaimId(scAlice, jvb, splitRewardQuorum, mcAlice), Ter(tecXCHAIN_REWARD_MISMATCH)) .close(); // A reward amount that isn't XRP XEnv(*this, true) .tx(createBridge(Account::kMaster, jvb)) .close() .tx(xchainCreateClaimId(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(createBridge(Account::kMaster, jvb)) .close() .tx(xchainCreateClaimId(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(createBridge(Account::kMaster, jvb)) .disableFeature(featureXChainBridge) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice), Ter(temDISABLED)) .close(); } void testXChainCommit() { using namespace jtx; XRPAmount const res0 = reserve(0); XRPAmount const fee = txFee(); testcase("Commit"); // Commit to a non-existent bridge XEnv(*this).tx(xchainCommit(mcAlice, jvb, 1, oneXrp, scBob), Ter(tecNO_ENTRY)); // check that reward not deducted when doing the commit { XEnv xenv(*this); test::Balance const aliceBal(xenv, mcAlice); auto const amt = XRP(1000); xenv.tx(createBridge(mcDoor, jvb)) .close() .tx(xchainCommit(mcAlice, jvb, 1, amt, scBob)) .close(); STAmount const claimCost = amt; BEAST_EXPECT(aliceBal.diff() == -(claimCost + fee)); } // Commit a negative amount XEnv(*this) .tx(createBridge(mcDoor, jvb)) .close() .tx(xchainCommit(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(createBridge(mcDoor, jvb)) .close() .tx(xchainCommit(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(createBridge(mcDoor, jvb)) .fund(res0 + oneXrp - xrpDust, mcuAlice) // barely not enough .close() .tx(xchainCommit(mcuAlice, jvb, 1, oneXrp, scBob), Ter(tecUNFUNDED_PAYMENT)); XEnv(*this) .tx(createBridge(mcDoor, jvb)) .fund( res0 + oneXrp + xrpDust, // "xrp_dust" for tx fees mcuAlice) // exactly enough => should succeed .close() .tx(xchainCommit(mcuAlice, jvb, 1, oneXrp, scBob)); // Commit an amount above the account's balance (for both XRP and // IOUs) XEnv(*this) .tx(createBridge(mcDoor, jvb)) .fund(res0, mcuAlice) // barely not enough .close() .tx(xchainCommit(mcuAlice, jvb, 1, res0 + oneXrp, scBob), Ter(tecUNFUNDED_PAYMENT)); auto jvbUsd = 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(createBridge(mcDoor, jvbUsd)) .close() .tx(xchainCommit(mcGw, jvbUsd, 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(createBridge(mcDoor, jvbUsd)) .close() .tx(xchainCommit(mcDoor, jvbUsd, 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(createBridge(mcDoor, jvbUsd)) .close() .tx(xchainCommit(mcAlice, jvbUsd, 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(createBridge(mcDoor, jvbUsd)) .close() .tx(xchainCommit(mcAlice, jvbUsd, 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(createBridge(mcDoor, jvbUsd)) .close() //.tx(pay(mcAlice, mcDoor, mcUSD(10))); .tx(xchainCommit(mcAlice, jvbUsd, 1, mcUSD(10), scAlice)); // coverage test: xchain_commit transaction with incorrect flag XEnv(*this) .tx(createBridge(mcDoor)) .close() .tx(xchainCommit(mcAlice, jvb, 1, oneXrp, scBob), Txflags(tfFillOrKill), Ter(temINVALID_FLAG)); // coverage test: xchain_commit transaction with xchain feature // disabled XEnv(*this) .tx(createBridge(mcDoor)) .disableFeature(featureXChainBridge) .close() .tx(xchainCommit(mcAlice, jvb, 1, oneXrp, scBob), Ter(temDISABLED)); } void testXChainAddAttestation() { using namespace jtx; testcase("Add Attestation"); XRPAmount const res0 = reserve(0); XRPAmount fee = txFee(); auto multiTtxFee = [&](std::uint32_t m) -> STAmount { return multiply(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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id present auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer(scEnv, Account::kMaster, scBob, scAlice, payees, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers, kUtXchainDefaultQuorum)) .close(); scEnv .tx(claimAttestation( scAttester, jvb, mcAlice, amt, payees[kUtXchainDefaultQuorum], true, claimID, dst, signers[kUtXchainDefaultQuorum])) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scBob)).close(); BEAST_EXPECT(!scEnv.claimID(jvb, claimID)); // claim id deleted BEAST_EXPECT(scEnv.claimID(jvb) == claimID); } BEAST_EXPECT(transfer.hasHappened(amt, splitRewardEveryone)); } // 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 quorum7 = 7; std::vector const signers = [] { static constexpr int kNumSigners = 4; std::uint32_t const weights[] = {1, 2, 4, 4}; std::vector result; result.reserve(kNumSigners); for (int i = 0; i < kNumSigners; ++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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum7, signers)) .close() .tx(xchainCreateClaimId(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{scBob}); auto const amt = XRP(1000); mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], 3, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers, 3)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scBob)).close(); } BEAST_EXPECT(!scEnv.claimID(jvb, 1)); // claim id deleted BEAST_EXPECT(transfer.hasHappened(amt, divide(reward, STAmount(3), reward.asset()))); } // 4,4 => should succeed for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); std::uint32_t const quorum7 = 7; std::vector const signers = [] { static constexpr int kNumSigners = 4; std::uint32_t const weights[] = {1, 2, 4, 4}; std::vector result; result.reserve(kNumSigners); for (int i = 0; i < kNumSigners; ++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 splitReward = divide(reward, STAmount(signers.size()), reward.asset()); mcEnv.tx(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum7, signers)) .close() .tx(xchainCreateClaimId(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{scBob}); auto const amt = XRP(1000); mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[2], 2, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers, 2, 2)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scBob)).close(); } BEAST_EXPECT(!scEnv.claimID(jvb, claimID)); // claim id deleted BEAST_EXPECT(transfer.hasHappened(amt, divide(reward, STAmount(2), reward.asset()))); } // 1,2 => should fail for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); std::uint32_t const quorum7 = 7; std::vector const signers = [] { static constexpr int kNumSigners = 4; std::uint32_t const weights[] = {1, 2, 4, 4}; std::vector result; result.reserve(kNumSigners); for (int i = 0; i < kNumSigners; ++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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum7, signers)) .close() .tx(xchainCreateClaimId(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{scBob}); auto const amt = XRP(1000); mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], 2, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers, 2)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv .tx(xchainClaim(scAlice, jvb, claimID, amt, scBob), Ter(tecXCHAIN_CLAIM_NO_QUORUM)) .close(); } BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id still present BEAST_EXPECT(transfer.hasNotHappened()); } // 2,4 => should fail for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); std::uint32_t const quorum7 = 7; std::vector const signers = [] { static constexpr int kNumSigners = 4; std::uint32_t const weights[] = {1, 2, 4, 4}; std::vector result; result.reserve(kNumSigners); for (int i = 0; i < kNumSigners; ++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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum7, signers)) .close() .tx(xchainCreateClaimId(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{scBob}); auto const amt = XRP(1000); mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[1], 2, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers, 2, 1)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv .tx(xchainClaim(scAlice, jvb, claimID, amt, scBob), Ter(tecXCHAIN_CLAIM_NO_QUORUM)) .close(); } BEAST_EXPECT(!!scEnv.claimID(jvb, claimID)); // claim id still present BEAST_EXPECT(transfer.hasNotHappened()); } // 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 amtPlusReward = amt + reward; { test::Balance const door(mcEnv, mcDoor); test::Balance const carol(mcEnv, mcCarol); mcEnv.tx(createBridge(mcDoor, jvb, reward, XRP(20))) .close() .tx(sidechainXchainAccountCreate(mcAlice, jvb, scuAlice, amt, reward)) .tx(sidechainXchainAccountCreate(mcBob, jvb, scuBob, amt, reward)) .tx(sidechainXchainAccountCreate(mcCarol, jvb, scuCarol, amt, reward)) .close(); BEAST_EXPECT( door.diff() == (multiply(amtPlusReward, STAmount(3), xrpIssue()) - fee)); BEAST_EXPECT(carol.diff() == -(amt + reward + fee)); } scEnv.tx(createBridge(Account::kMaster, jvb, reward, XRP(20))) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close(); { // send first batch of account create attest for all 3 // account create test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); scEnv.multiTx(attCreateAcctVec(1, amt, scuAlice, 2)) .multiTx(attCreateAcctVec(3, amt, scuCarol, 2)) .multiTx(attCreateAcctVec(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 test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); scEnv.multiTx(attCreateAcctVec(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 test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); scEnv.multiTx(attCreateAcctVec(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 test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); scEnv.multiTx(attCreateAcctVec(1, amt, scuAlice, 3, 1)).close(); BEAST_EXPECT(door.diff() == -amtPlusReward); // 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 test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); scEnv.multiTx(attCreateAcctVec(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 test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); scEnv.multiTx(attCreateAcctVec(2, amt, scuBob, 1)).close(); BEAST_EXPECT(door.diff() == -amtPlusReward); BEAST_EXPECT(attester.diff() == -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 test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); scEnv.multiTx(attCreateAcctVec(3, amt, scuCarol, 1)).close(); BEAST_EXPECT(door.diff() == -amtPlusReward); BEAST_EXPECT(attester.diff() == -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 amtPlusReward = amt + reward; mcEnv.tx(createBridge(mcDoor, jvb, reward, XRP(20))).close(); { test::Balance const door(mcEnv, mcDoor); test::Balance const carol(mcEnv, mcCarol); mcEnv.tx(sidechainXchainAccountCreate(mcCarol, jvb, scuAlice, amt, reward)).close(); BEAST_EXPECT(door.diff() == amtPlusReward); BEAST_EXPECT(carol.diff() == -(amtPlusReward + fee)); } scEnv.tx(createBridge(Account::kMaster, jvb, reward, XRP(20))) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close(); test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); scEnv.multiTx(attCreateAcctVec(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(attCreateAcctVec(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 amtPlusReward = amt + reward; mcEnv.tx(createBridge(mcDoor, jvb, reward, XRP(20))).close(); { test::Balance const door(mcEnv, mcDoor); test::Balance const carol(mcEnv, mcCarol); mcEnv.tx(sidechainXchainAccountCreate(mcCarol, jvb, scAlice, amt, reward)).close(); BEAST_EXPECT(door.diff() == amtPlusReward); BEAST_EXPECT(carol.diff() == -(amtPlusReward + fee)); } scEnv.tx(createBridge(Account::kMaster, jvb, reward, XRP(20))) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close(); test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); test::Balance const alice(scEnv, scAlice); scEnv.multiTx(attCreateAcctVec(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(attCreateAcctVec(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() == -amtPlusReward); 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 amtPlusReward = amt + reward; mcEnv.tx(createBridge(mcDoor, jvb, reward, XRP(20))).close(); { test::Balance const door(mcEnv, mcDoor); test::Balance const carol(mcEnv, mcCarol); mcEnv.tx(sidechainXchainAccountCreate(mcCarol, jvb, scAlice, amt, reward)).close(); BEAST_EXPECT(door.diff() == amtPlusReward); BEAST_EXPECT(carol.diff() == -(amtPlusReward + fee)); } scEnv.tx(createBridge(Account::kMaster, jvb, reward, XRP(20))) .tx(jtx::signers(Account::kMaster, quorum, signers)) .tx(fset("scAlice", asfDepositAuth)) // set deposit auth .close(); test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); test::Balance const alice(scEnv, scAlice); scEnv.multiTx(attCreateAcctVec(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(attCreateAcctVec(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 amtPlusReward = amt + reward; { test::Balance const door(mcEnv, mcDoor); test::Balance const carol(mcEnv, mcCarol); mcEnv.tx(createBridge(mcDoor, jvb, reward, XRP(20))) .close() .tx(sidechainXchainAccountCreate(mcAlice, jvb, scuAlice, amt, reward)) .close() // make sure Alice gets claim #1 .tx(sidechainXchainAccountCreate(mcBob, jvb, scuBob, amt, reward)) .close() // make sure Bob gets claim #2 .tx(sidechainXchainAccountCreate(mcCarol, jvb, scuCarol, amt, reward)) .close(); // and Carol will get claim #3 BEAST_EXPECT( door.diff() == (multiply(amtPlusReward, STAmount(3), xrpIssue()) - fee)); BEAST_EXPECT(carol.diff() == -(amt + reward + fee)); } std::uint32_t const redQuorum = 2; scEnv.tx(createBridge(Account::kMaster, jvb, reward, XRP(20))) .tx(jtx::signers(Account::kMaster, redQuorum, signers)) .close(); { test::Balance const attester(scEnv, scAttester); test::Balance const door(scEnv, Account::kMaster); auto const badAmt = XRP(10); std::uint32_t txCount = 0; // send attestations with incorrect amounts to for all 3 // AccountCreate. They will be replaced later scEnv.multiTx(attCreateAcctVec(1, badAmt, scuAlice, 1)) .multiTx(attCreateAcctVec(2, badAmt, scuBob, 1, 2)) .multiTx(attCreateAcctVec(3, badAmt, 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(attCreateAcctVec(1, amt, scuAlice, 1, 0)) .multiTx(attCreateAcctVec(2, amt, scuBob, 1, 2)) .multiTx(attCreateAcctVec(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(attCreateAcctVec(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(attCreateAcctVec(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(attCreateAcctVec(2, amt, scuBob, 1, 3)) .multiTx( attCreateAcctVec(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(attCreateAcctVec(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(amtPlusReward, 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(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(claimAttestation( 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(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .disableFeature(featureXChainBridge) .close() .tx(claimAttestation( 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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(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(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); auto const dstStartBalance = scEnv.env.balance(dst); for (int i = 0; i < signers.size(); ++i) { auto const att = claimAttestation( 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 < kUtXchainDefaultNumSigners - 2; ++i) scEnv.fund(amt, altSigners[i].account); mcEnv.tx(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, altSigners)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); Account const dst{scBob}; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); auto const dstStartBalance = scEnv.env.balance(dst); { // G1: master key auto att = claimAttestation( scAttester, jvb, mcAlice, amt, payees[0], true, claimID, dst, altSigners[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(altSigners[1].account, altSigners[0].account)).close(); auto att = claimAttestation( scAttester, jvb, mcAlice, amt, payees[1], true, claimID, dst, altSigners[0]); att[sfAttestationSignerAccount.getJsonName()] = altSigners[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 = altSigners[kUtXchainDefaultNumSigners - 1]; auto const unfundedSigner2 = altSigners[kUtXchainDefaultNumSigners - 2]; auto att = claimAttestation( scAttester, jvb, mcAlice, amt, payees[kUtXchainDefaultNumSigners - 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 tempSignerList = {signers[0]}; scEnv.tx(jtx::signers(altSigners[2].account, 1, tempSignerList)); auto att = claimAttestation( scAttester, jvb, mcAlice, amt, payees[2], true, claimID, dst, tempSignerList.front()); att[sfAttestationSignerAccount.getJsonName()] = altSigners[2].account.human(); scEnv.tx(att, Ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR)).close(); } { // B1: disabled master key scEnv.tx(fset(altSigners[2].account, asfDisableMaster, 0)).close(); auto att = claimAttestation( scAttester, jvb, mcAlice, amt, payees[2], true, claimID, dst, altSigners[2]); scEnv.tx(att, Ter(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR)).close(); } { // --B4: not on signer list auto att = claimAttestation( 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 = claimAttestation( scAttester, jvb, mcAlice, amt, payees[3], true, claimID, dst, altSigners[3]); att.removeMember(sfAttestationSignerAccount.getJsonName()); scEnv.tx(att, Ter(temMALFORMED)).close(); BEAST_EXPECT(dstStartBalance == scEnv.env.balance(dst)); att[sfAttestationSignerAccount.getJsonName()] = altSigners[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 const txFee = mcEnv.txFee(); Account const a{"a"}; Account const doorA{"doorA"}; STAmount const funds{XRP(10000)}; mcEnv.fund(funds, a); mcEnv.fund(funds, doorA); Account const ua{"ua"}; // unfunded account we want to create BridgeDef xrpB{ .doorA = doorA, .issueA = xrpIssue(), .doorB = Account::kMaster, .issueB = xrpIssue(), .reward = XRP(1), // reward .minAccountCreate = XRP(20), // minAccountCreate .quorum = 4, // quorum .signers = signers, .jvb = json::ValueType::Null}; xrpB.initBridge(mcEnv, scEnv); auto const amt = XRP(777); auto const amtPlusReward = amt + xrpB.reward; { test::Balance const balDoorA(mcEnv, doorA); test::Balance const balA(mcEnv, a); mcEnv.tx(sidechainXchainAccountCreate(a, xrpB.jvb, ua, amt, xrpB.reward)).close(); BEAST_EXPECT(balDoorA.diff() == amtPlusReward); BEAST_EXPECT(balA.diff() == -(amtPlusReward + txFee)); } for (int i = 0; i < signers.size(); ++i) { auto const att = createAccountAttestation( signers[0].account, xrpB.jvb, a, amt, xrpB.reward, signers[i].account, true, 1, ua, signers[i]); TER const expectedTER = i < xrpB.quorum ? tesSUCCESS : TER{tecXCHAIN_ACCOUNT_CREATE_PAST}; scEnv.tx(att, Ter(expectedTER)).close(); if (i + 1 < xrpB.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 const res0 = reserve(0); XRPAmount const 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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scBob)).close(); } BEAST_EXPECT(transfer.hasHappened(amt, splitRewardQuorum)); } // 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(createBridge(mcDoor, jvb)).close(); scEnv .tx(createBridge(Account::kMaster, jvb)) //.tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], 1, withClaim); jtx::Signer const masterSigner(Account::kMaster); scEnv .tx(claimAttestation( scAttester, jvb, mcAlice, amt, payees[0], true, claimID, dst, masterSigner), Ter(tecXCHAIN_NO_SIGNERS_LIST)) .close(); BEAST_EXPECT(transfer.hasNotHappened()); } // 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(createBridge(mcDoor, jvb)).close(); scEnv .tx(createBridge(Account::kMaster, jvb)) //.tx(jtx::signers(Account::kMaster, quorum, signers)) .tx(jtx::regkey(Account::kMaster, payees[0])) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], 1, withClaim); jtx::Signer const masterSigner(payees[0]); scEnv .tx(claimAttestation( scAttester, jvb, mcAlice, amt, payees[0], true, claimID, dst, masterSigner), Ter(tecXCHAIN_NO_SIGNERS_LIST)) .close(); BEAST_EXPECT(transfer.hasNotHappened()); } // Claim against non-existent bridge // --------------------------------- for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); mcEnv.tx(createBridge(mcDoor, jvb)).close(); auto jvbUnknown = bridge(mcBob, xrpIssue(), Account::kMaster, xrpIssue()); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvbUnknown, reward, mcAlice), Ter(tecNO_ENTRY)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvbUnknown, claimID, amt, dst), Ter(tecNO_ENTRY)) .close(); BalanceTransfer transfer(scEnv, Account::kMaster, scBob, scAlice, payees, withClaim); scEnv .tx(claimAttestation( scAttester, jvbUnknown, mcAlice, amt, payees[0], true, claimID, dst, signers[0]), Ter(tecNO_ENTRY)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvbUnknown, claimID, amt, scBob), Ter(tecNO_ENTRY)) .close(); } BEAST_EXPECT(transfer.hasNotHappened()); } // Claim against non-existent claim id // ----------------------------------- for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); mcEnv.tx(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer(scEnv, Account::kMaster, scBob, scAlice, payees, withClaim); // attest using non-existent claim id scEnv .tx(claimAttestation( scAttester, jvb, mcAlice, amt, payees[0], true, 999, dst, signers[0]), Ter(tecXCHAIN_NO_CLAIM_ID)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // claim using non-existent claim id scEnv.tx(xchainClaim(scAlice, jvb, 999, amt, scBob), Ter(tecXCHAIN_NO_CLAIM_ID)) .close(); } BEAST_EXPECT(transfer.hasNotHappened()); } // Claim against a claim id owned by another account // ------------------------------------------------- for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); mcEnv.tx(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // submit a claim transaction with the wrong account (scGw // instead of scAlice) scEnv.tx(xchainClaim(scGw, jvb, claimID, amt, scBob), Ter(tecXCHAIN_BAD_CLAIM_ID)) .close(); BEAST_EXPECT(transfer.hasNotHappened()); } else { BEAST_EXPECT(transfer.hasHappened(amt, splitRewardQuorum)); } } // Claim against a claim id with no attestations // --------------------------------------------- for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); mcEnv.tx(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer(scEnv, Account::kMaster, scBob, scAlice, payees, withClaim); // don't send any attestations if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv .tx(xchainClaim(scAlice, jvb, claimID, amt, scBob), Ter(tecXCHAIN_CLAIM_NO_QUORUM)) .close(); } BEAST_EXPECT(transfer.hasNotHappened()); } // 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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer(scEnv, Account::kMaster, scBob, scAlice, payees, withClaim); auto tooFew = quorum - 1; scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers, tooFew)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv .tx(xchainClaim(scAlice, jvb, claimID, amt, scBob), Ter(tecXCHAIN_CLAIM_NO_QUORUM)) .close(); } BEAST_EXPECT(transfer.hasNotHappened()); } // Claim id of zero // ---------------- for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); mcEnv.tx(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer(scEnv, Account::kMaster, scBob, scAlice, payees, withClaim); scEnv .multiTx( claimAttestations(scAttester, jvb, mcAlice, amt, payees, true, 0, dst, signers), Ter(tecXCHAIN_NO_CLAIM_ID)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, 0, amt, scBob), Ter(tecXCHAIN_NO_CLAIM_ID)) .close(); } BEAST_EXPECT(transfer.hasNotHappened()); } // 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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, scUSD(1000), scBob), Ter(temBAD_AMOUNT)) .close(); } BEAST_EXPECT(transfer.hasNotHappened()); } // 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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scuBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scuBob), Ter(tecNO_DST)).close(); } BEAST_EXPECT(transfer.hasNotHappened()); } // 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(createBridge(mcDoor, jvb)).close(); STAmount const hugeReward{XRP(20000)}; BEAST_EXPECT(hugeReward > scEnv.balance(scAlice)); scEnv.tx(createBridge(Account::kMaster, jvb, hugeReward)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, hugeReward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); if (withClaim) { scEnv .multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)) .close(); BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scBob), Ter(tecUNFUNDED_PAYMENT)) .close(); } else { auto txns = claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers); for (int i = 0; i < kUtXchainDefaultQuorum - 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(xchainClaim(scAlice, jvb, claimID, amt, scBob), Ter(tecUNFUNDED_PAYMENT)) .close(); } BEAST_EXPECT(transfer.hasNotHappened()); } // 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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .fund( res0 + reward, scuAlice) // just not enough because of fees .close() .tx(xchainCreateClaimId(scuAlice, jvb, reward, mcAlice), Ter(tecINSUFFICIENT_RESERVE)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer(scEnv, Account::kMaster, scBob, scuAlice, payees, withClaim); scEnv .tx(claimAttestation( scAttester, jvb, mcAlice, amt, payees[0], true, claimID, dst, signers[0]), Ter(tecXCHAIN_NO_CLAIM_ID)) .close(); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv .tx(xchainClaim(scuAlice, jvb, claimID, amt, scBob), Ter(tecXCHAIN_NO_CLAIM_ID)) .close(); } BEAST_EXPECT(transfer.hasNotHappened()); } // Pay to an account with deposit auth set // --------------------------------------- for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); mcEnv.tx(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .tx(fset("scBob", asfDepositAuth)) // set deposit auth .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); auto txns = claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers); for (int i = 0; i < kUtXchainDefaultQuorum - 1; ++i) { scEnv.tx(txns[i]).close(); } if (withClaim) { scEnv.tx(txns.back()).close(); BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(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 test::Balance const scCarolBal(scEnv, scCarol); scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scCarol)).close(); BEAST_EXPECT(scCarolBal.diff() == amt); } else { scEnv.tx(txns.back()).close(); scEnv.tx(xchainClaim(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(); test::Balance const scBobBal(scEnv, scBob); scEnv.tx(txns.back()).close(); BEAST_EXPECT(scBobBal.diff() == amt); } } // Pay to an account with Destination Tag set // ------------------------------------------ for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); mcEnv.tx(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .tx(fset("scBob", asfRequireDest)) // set dest tag .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); auto txns = claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers); for (int i = 0; i < kUtXchainDefaultQuorum - 1; ++i) { scEnv.tx(txns[i]).close(); } if (withClaim) { scEnv.tx(txns.back()).close(); BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(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 test::Balance const scCarolBal(scEnv, scCarol); scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scCarol)).close(); BEAST_EXPECT(scCarolBal.diff() == amt); } else { scEnv.tx(txns.back()).close(); scEnv.tx(xchainClaim(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(); test::Balance const scBobBal(scEnv, scBob); scEnv.tx(txns.back()).close(); BEAST_EXPECT(scBobBal.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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .tx(fset("scBob", asfDepositAuth)) // set deposit auth .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(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 test::Balance const scBobBal(scEnv, scBob); scEnv.multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)); BEAST_EXPECT(scBobBal.diff() == STAmount(0)); // Check that check that we still can use the claimID to transfer // the amount to a different account test::Balance const scCarolBal(scEnv, scCarol); scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scCarol)).close(); BEAST_EXPECT(scCarolBal.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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); scEnv.multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // claim wrong amount scEnv .tx(xchainClaim(scAlice, jvb, claimID, oneXrp, scBob), Ter(tecXCHAIN_CLAIM_NO_QUORUM)) .close(); } BEAST_EXPECT(transfer.hasNotHappened()); } // 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(createBridge(mcDoor, jvb)).close(); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); test::Balance const scAliceBal(scEnv, scAlice); scEnv.multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)); STAmount claimCost = reward; if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scBob)).close(); claimCost += fee; } BEAST_EXPECT(transfer.hasHappened(amt, splitRewardQuorum)); BEAST_EXPECT(scAliceBal.diff() == -claimCost); // 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(createBridge(mcDoor, jvb, tinyReward)).close(); scEnv.tx(createBridge(Account::kMaster, jvb, tinyReward)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, tinyReward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum, withClaim); test::Balance const scAliceBal(scEnv, scAlice); scEnv.multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)); STAmount claimCost = tinyReward; if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(scAlice, jvb, claimID, amt, scBob)).close(); claimCost += fee; } BEAST_EXPECT(transfer.hasHappened(amt, tinyRewardSplit)); BEAST_EXPECT(scAliceBal.diff() == -(claimCost - tinyRewardRemainder)); } // 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(createBridge(mcDoor, jvb)).close(); std::vector altPayees{payees.begin(), payees.end() - 1}; altPayees.back() = Account("inexistent"); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum - 1, withClaim); scEnv.multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, altPayees, true, claimID, dst, signers)); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(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.hasHappened(amt, splitRewardQuorum)); } for (auto withClaim : {false, true}) { XEnv mcEnv(*this); XEnv scEnv(*this, true); mcEnv.tx(createBridge(mcDoor, jvb)).close(); auto& unpaid = payees[kUtXchainDefaultQuorum - 1]; scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .tx(fset(unpaid, asfDepositAuth)) .close() .tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)) .close(); auto dst(withClaim ? std::nullopt : std::optional{scBob}); auto const amt = XRP(1000); std::uint32_t const claimID = 1; mcEnv.tx(xchainCommit(mcAlice, jvb, claimID, amt, dst)).close(); // balance of last signer should not change (has deposit auth) test::Balance const lastSigner(scEnv, unpaid); // make sure all signers except the last one get the // split_reward BalanceTransfer transfer( scEnv, Account::kMaster, scBob, scAlice, &payees[0], kUtXchainDefaultQuorum - 1, withClaim); scEnv.multiTx(claimAttestations( scAttester, jvb, mcAlice, amt, payees, true, claimID, dst, signers)); if (withClaim) { BEAST_EXPECT(transfer.hasNotHappened()); // need to submit a claim transactions scEnv.tx(xchainClaim(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.hasHappened(amt, splitRewardQuorum)); // and make sure the account with deposit auth received nothing BEAST_EXPECT(lastSigner.diff() == STAmount(0)); } // coverage test: xchain_claim transaction with incorrect flag XEnv(*this, true) .tx(createBridge(Account::kMaster, jvb)) .close() .tx(xchainClaim(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(createBridge(Account::kMaster, jvb)) .disableFeature(featureXChainBridge) .close() .tx(xchainClaim(scAlice, jvb, 1, XRP(1000), scBob), Ter(temDISABLED)) .close(); // coverage test: XChainClaim::preclaim - isLockingChain = true; XEnv(*this) .tx(createBridge(mcDoor, jvb)) .close() .tx(xchainClaim(mcAlice, jvb, 1, XRP(1000), mcBob), Ter(tecXCHAIN_NO_CLAIM_ID)); } void testXChainCreateAccount() { using namespace jtx; testcase("Bridge Create Account"); XRPAmount const fee = txFee(); // coverage test: transferHelper() - dst == src { XEnv scEnv(*this, true); auto const amt = XRP(111); auto const amtPlusReward = amt + reward; scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close(); test::Balance const door(scEnv, Account::kMaster); // scEnv.tx(att_create_acct_batch1(1, amt, // Account::kMaster)).close(); scEnv.multiTx(attCreateAcctVec(1, amt, Account::kMaster, 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::kMaster)).close(); scEnv.multiTx(attCreateAcctVec(1, amt, Account::kMaster, 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(createBridge(mcDoor, jvb, XRP(1), XRP(20))).close(); test::Balance const door(mcEnv, mcDoor); test::Balance const carol(mcEnv, mcCarol); mcEnv .tx(sidechainXchainAccountCreate(mcCarol, jvb, scuAlice, XRP(19), reward), Ter(tecXCHAIN_INSUFF_CREATE_AMOUNT)) .close(); BEAST_EXPECT(door.diff() == STAmount(0)); BEAST_EXPECT(carol.diff() == -fee); } // Check that creating an account with invalid flags fails. { XEnv mcEnv(*this); mcEnv.tx(createBridge(mcDoor, jvb, XRP(1), XRP(20))).close(); test::Balance const door(mcEnv, mcDoor); mcEnv .tx(sidechainXchainAccountCreate(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(createBridge(mcDoor, jvb, XRP(1), XRP(20))).close(); test::Balance const door(mcEnv, mcDoor); mcEnv.disableFeature(featureXChainBridge) .tx(sidechainXchainAccountCreate(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(createBridge(mcDoor, jvb, XRP(1), XRP(20))).close(); test::Balance const door(mcEnv, mcDoor); mcEnv .tx(sidechainXchainAccountCreate(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(createBridge(mcDoor, jvb, XRP(1), XRP(20))).close(); test::Balance const door(mcEnv, mcDoor); mcEnv .tx(sidechainXchainAccountCreate(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(createBridge(mcDoor, jvb, XRP(1), XRP(20))).close(); test::Balance const door(mcEnv, mcDoor); mcEnv .tx(sidechainXchainAccountCreate(mcDoor, jvb, scuAlice, XRP(20), XRP(1)), Ter(tecXCHAIN_SELF_COMMIT)) .close(); BEAST_EXPECT(door.diff() == -fee); } // Check that reward matches the amount specified in bridge { XEnv mcEnv(*this); mcEnv.tx(createBridge(mcDoor, jvb, XRP(1), XRP(20))).close(); test::Balance const door(mcEnv, mcDoor); mcEnv .tx(sidechainXchainAccountCreate(mcCarol, jvb, scuAlice, XRP(20), XRP(2)), Ter(tecXCHAIN_REWARD_MISMATCH)) .close(); BEAST_EXPECT(door.diff() == STAmount(0)); } } void testFeeDipsIntoReserve() { using namespace jtx; XRPAmount const res0 = reserve(0); XRPAmount const fee = txFee(); testcase("Fee dips into reserve"); // commit where the fee dips into the reserve, this should succeed XEnv(*this) .tx(createBridge(mcDoor, jvb)) .fund(res0 + oneXrp + fee - drops(1), mcuAlice) .close() .tx(xchainCommit(mcuAlice, jvb, 1, oneXrp, scBob), Ter(tesSUCCESS)); // commit where the commit amount drips into the reserve, this should // fail XEnv(*this) .tx(createBridge(mcDoor, jvb)) .fund(res0 + oneXrp - drops(1), mcuAlice) .close() .tx(xchainCommit(mcuAlice, jvb, 1, oneXrp, 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(createBridge(mcDoor, jvb, reward, minAccountCreate)) .fund(res0 + fee + minAccountCreate + reward - drops(1), mcuAlice) .close() .tx(sidechainXchainAccountCreate(mcuAlice, jvb, scuAlice, minAccountCreate, reward), Ter(tesSUCCESS)); // account create commit where the commit dips into the reserve, // this should fail XEnv(*this) .tx(createBridge(mcDoor, jvb, reward, minAccountCreate)) .fund(res0 + minAccountCreate + reward - drops(1), mcuAlice) .close() .tx(sidechainXchainAccountCreate(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(createBridge(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(createBridge(Account::kMaster, jvb)) .close() .tx(xchainCreateClaimId(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 const dst{scBob}; auto const amt = XRP(1000); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close(); scEnv.tx(xchainCreateClaimId(scAlice, jvb, reward, mcAlice)).close(); auto jvAtt = claimAttestation( scAttester, jvb, mcAlice, amt, payees[kUtXchainDefaultQuorum], true, claimID, dst, signers[kUtXchainDefaultQuorum]); { // 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 const dst{scBob}; auto const amt = XRP(1000); auto const rewardAmt = XRP(1); scEnv.tx(createBridge(Account::kMaster, jvb)) .tx(jtx::signers(Account::kMaster, quorum, signers)) .close(); auto jvAtt = createAccountAttestation( scAttester, jvb, mcAlice, amt, rewardAmt, payees[kUtXchainDefaultQuorum], true, createCount, dst, signers[kUtXchainDefaultQuorum]); { // 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 kNumSigners = 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 withClaim{WithClaim::No}; uint32_t claimId{0}; std::array attested{}; }; struct AccountCreate { jtx::Account from; jtx::Account to; STAmount amt; STAmount reward; bool a2b; uint32_t claimId{0}; std::array attested{}; }; using ENV = XEnv; 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 const diff{env.balance(acct) - startAmount}; bool const check = diff == expectedDiff; return check; } }; // -------------------------------------------------- struct ChainStateTrack { using ClaimVec = jtx::JValueVec; using CreateClaimVec = jtx::JValueVec; using CreateClaimMap = std::map; ChainStateTrack(ENV& env) : env(env), txFee(env.env.current()->fees().base) { } void sendAttestations(size_t signerIdx, BridgeID bridge, ClaimVec& claims) { for (auto const& c : claims) { env.tx(c).close(); spendFee(bridge->signers[signerIdx].account); } claims.clear(); } uint32_t sendCreateAttestations(size_t signerIdx, BridgeID bridge, CreateClaimVec& claims) { size_t numSuccessful = 0; for (auto const& c : claims) { env.tx(c).close(); if (env.ter() == tesSUCCESS) { counters[bridge].signers.push_back(signerIdx); numSuccessful++; } spendFee(bridge->signers[signerIdx].account); } claims.clear(); return numSuccessful; } void sendAttestations() { bool callbackCalled = 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 { callbackCalled = false; // cspell: ignore attns for (size_t i = 0; i < signersAttns.size(); ++i) { for (auto& [bridge, claims] : signersAttns[i]) { sendAttestations(i, bridge, claims.xferClaims); auto& c = counters[bridge]; auto& createClaims = claims.createClaims[c.claimCount]; auto numAttns = createClaims.size(); if (numAttns != 0u) { c.numCreateAttnSent += sendCreateAttestations(i, bridge, createClaims); } assert(claims.createClaims[c.claimCount].empty()); } } for (auto& [bridge, c] : counters) { if (c.numCreateAttnSent >= bridge->quorum) { callbackCalled = true; c.createCallbacks[c.claimCount](c.signers); ++c.claimCount; c.numCreateAttnSent = 0; c.signers.clear(); } } } while (callbackCalled); } 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.asset() != 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.asset(), divisor), amt.asset())); } } void spend(jtx::Account const& acct, STAmount amt, std::uint64_t times = 1) { if (amt.asset() != xrpIssue()) return; receive( acct, times == 1 ? -amt : -multiply(amt, STAmount(amt.asset(), times), amt.asset())); } 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, txFee, times); } [[nodiscard]] 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 const& signers)>; uint32_t claimId{0}; uint32_t createCount{0}; // for account create. First should be 1 uint32_t claimCount{0}; // for account create. Increments after quorum for // current createCount (starts at 1) is reached. uint32_t numCreateAttnSent{0}; // for current claimCount std::vector signers; std::vector createCallbacks; }; struct Claims { ClaimVec xferClaims; CreateClaimMap createClaims; }; using SignerAttns = std::unordered_map; using SignersAttns = std::array; ENV& env; std::map accounts; SignersAttns signersAttns; std::map counters; STAmount txFee; }; struct ChainStateTracker { ChainStateTracker(ENV& aEnv, ENV& bEnv) : a(aEnv), b(bEnv) { } [[nodiscard]] 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 class SmState { Initial, ClaimIdCreated, Attesting, Attested, Completed, Closed, }; enum class ActFlags { A2b = 1 << 0 }; // -------------------------------------------------- template class SmBase { SmBase(std::shared_ptr const& chainstate, BridgeDef const& bridge) : bridge_(bridge), st_(chainstate) { } public: ChainStateTrack& srcState() { return static_cast(*this).a2b() ? st_->a : st_->b; } ChainStateTrack& destState() { return static_cast(*this).a2b() ? st_->b : st_->a; } jtx::Account const& srcDoor() { return static_cast(*this).a2b() ? bridge_.doorA : bridge_.doorB; } jtx::Account const& dstDoor() { return static_cast(*this).a2b() ? bridge_.doorB : bridge_.doorA; } protected: BridgeDef const& bridge_; std::shared_ptr st_; friend T; }; // -------------------------------------------------- class SmCreateAccount : public SmBase { public: using Base = SmBase; SmCreateAccount( std::shared_ptr const& chainstate, BridgeDef const& bridge, AccountCreate create) : Base(chainstate, bridge), cr_(std::move(create)) { } [[nodiscard]] bool a2b() const { return cr_.a2b; } uint32_t issueAccountCreate() { ChainStateTrack& st = srcState(); jtx::Account const& srcdoor = srcDoor(); st.env .tx(sidechainXchainAccountCreate( 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_].createCount; } 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 < kNumSigners; ++i) { size_t const signerIdx = (rnd + i) % kNumSigners; if (!(cr_.attested[signerIdx])) { // enqueue one attestation for this signer cr_.attested[signerIdx] = true; st.signersAttns[signerIdx][&bridge_].createClaims[cr_.claimId - 1].emplace_back( createAccountAttestation( bridge_.signers[signerIdx].account, bridge_.jvb, cr_.from, cr_.amt, cr_.reward, bridge_.signers[signerIdx].account, cr_.a2b, cr_.claimId, cr_.to, bridge_.signers[signerIdx])); break; } } if (i == kNumSigners) return; // did not attest auto& counters = st.counters[&bridge_]; if (counters.createCallbacks.size() < cr_.claimId) counters.createCallbacks.resize(cr_.claimId); auto completeCb = [&](std::vector const& signers) { auto numAttestors = signers.size(); st.env.close(); assert(numAttestors <= std::count(cr_.attested.begin(), cr_.attested.end(), true)); assert(numAttestors >= bridge_.quorum); assert(cr_.claimId - 1 == counters.claimCount); auto r = cr_.reward; auto reward = divide(r, STAmount(numAttestors), r.asset()); for (auto i : signers) st.receive(bridge_.signers[i].account, reward); st.spend(dstDoor(), reward, numAttestors); st.transfer(dstDoor(), cr_.to, cr_.amt); st.env.env.memoize(cr_.to); smState_ = SmState::Completed; }; counters.createCallbacks[cr_.claimId - 1] = std::move(completeCb); } SmState advance(uint64_t time, uint32_t rnd) { switch (smState_) { case SmState::Initial: cr_.claimId = issueAccountCreate(); smState_ = SmState::Attesting; break; case SmState::Attesting: attest(time, rnd); break; default: assert(0); break; case SmState::Completed: break; // will get this once } return smState_; } private: SmState smState_{SmState::Initial}; AccountCreate cr_; }; // -------------------------------------------------- class SmTransfer : public SmBase { public: using Base = SmBase; SmTransfer( std::shared_ptr const& chainstate, BridgeDef const& bridge, Transfer xfer) : Base(chainstate, bridge), xfer_(std::move(xfer)) { } [[nodiscard]] bool a2b() const { return xfer_.a2b; } uint32_t createClaimId() { ChainStateTrack& st = destState(); st.env.tx(xchainCreateClaimId(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_].claimId; } void commit() { ChainStateTrack& st = srcState(); jtx::Account const& srcdoor = srcDoor(); if (xfer_.amt.asset() != xrpIssue()) { st.env.tx(pay(srcdoor, xfer_.from, xfer_.amt)); st.spendFee(srcdoor); } st.env.tx(xchainCommit( xfer_.from, bridge_.jvb, xfer_.claimId, xfer_.amt, xfer_.withClaim == WithClaim::Yes ? std::nullopt : std::optional(xfer_.finaldest))); st.spendFee(xfer_.from); st.transfer(xfer_.from, srcdoor, xfer_.amt); } void distributeReward(ChainStateTrack& st) { auto r = bridge_.reward; auto reward = divide(r, STAmount(bridge_.quorum), r.asset()); for (size_t i = 0; i < kNumSigners; ++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 < kNumSigners; ++i) { size_t const signerIdx = (rnd + i) % kNumSigners; if (!(xfer_.attested[signerIdx])) { // enqueue one attestation for this signer xfer_.attested[signerIdx] = true; st.signersAttns[signerIdx][&bridge_].xferClaims.emplace_back(claimAttestation( bridge_.signers[signerIdx].account, bridge_.jvb, xfer_.from, xfer_.amt, bridge_.signers[signerIdx].account, xfer_.a2b, xfer_.claimId, xfer_.withClaim == WithClaim::Yes ? std::nullopt : std::optional(xfer_.finaldest), bridge_.signers[signerIdx])); break; } } // return true if quorum was reached, false otherwise bool const quorum = std::count(xfer_.attested.begin(), xfer_.attested.end(), true) >= bridge_.quorum; if (quorum && xfer_.withClaim == WithClaim::No) { distributeReward(st); st.transfer(dstDoor(), xfer_.finaldest, xfer_.amt); } return quorum; } void claim() { ChainStateTrack& st = destState(); st.env.tx( xchainClaim(xfer_.to, bridge_.jvb, xfer_.claimId, xfer_.amt, xfer_.finaldest)); distributeReward(st); st.transfer(dstDoor(), xfer_.finaldest, xfer_.amt); st.spendFee(xfer_.to); } SmState advance(uint64_t time, uint32_t rnd) { switch (smState_) { case SmState::Initial: xfer_.claimId = createClaimId(); smState_ = SmState::ClaimIdCreated; break; case SmState::ClaimIdCreated: commit(); smState_ = SmState::Attesting; break; case SmState::Attesting: if (attest(time, rnd)) { smState_ = xfer_.withClaim == WithClaim::Yes ? SmState::Attested : SmState::Completed; } else { smState_ = SmState::Attesting; } break; case SmState::Attested: assert(xfer_.withClaim == WithClaim::Yes); claim(); smState_ = SmState::Completed; break; default: case SmState::Completed: assert(0); // should have been removed break; } return smState_; } private: Transfer xfer_; SmState smState_{SmState::Initial}; }; // -------------------------------------------------- using Sm = std::variant; using SmCont = std::list>; SmCont sm_; void xfer( uint64_t time, std::shared_ptr 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 const& chainstate, BridgeDef const& bridge, AccountCreate ac) { sm_.emplace_back(time, SmCreateAccount(chainstate, bridge, std::move(ac))); } public: void runSimulation(std::shared_ptr const& st, bool verifyBalances = true) { using namespace jtx; uint64_t time = 0; std::mt19937 gen(27); // Standard mersenne_twister_engine std::uniform_int_distribution distrib(0, 9); while (!sm_.empty()) { ++time; for (auto it = sm_.begin(); it != sm_.end();) { auto vis = [&](auto& sm) { uint32_t const rnd = distrib(gen); return sm.advance(time, rnd); }; auto& [t, sm] = *it; if (t <= time && std::visit(vis, sm) == SmState::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 (verifyBalances) { 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(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"); static constexpr size_t kNumAcct = 10; auto a = [&doorXRPLocking, &doorUSDLocking, &doorUSDIssuing]() { using namespace std::literals; std::vector result; result.reserve(kNumAcct); for (int i = 0; i < kNumAcct; ++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 const amt{XRP(100000)}; mcEnv.fund(amt, acct); scEnv.fund(amt, acct); } Account const usdLockingAcc{"USDLocking"}; IOU const usdLocking{usdLockingAcc["USD"]}; IOU const usdIssuing{doorUSDIssuing["USD"]}; mcEnv.fund(XRP(100000), usdLockingAcc); mcEnv.close(); mcEnv.tx(trust(doorUSDLocking, usdLocking(100000))); mcEnv.close(); mcEnv.tx(pay(usdLockingAcc, doorUSDLocking, usdLocking(50000))); for (int i = 0; i < a.size(); ++i) { auto& acct{a[i]}; if (i < kNumAcct) { 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::kMaster); // also create some unfunded accounts static constexpr size_t kNumUa = 20; auto ua = []() { using namespace std::literals; std::vector result; result.reserve(kNumUa); for (int i = 0; i < kNumUa; ++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 xrpB{ .doorA = doorXRPLocking, .issueA = xrpIssue(), .doorB = Account::kMaster, .issueB = xrpIssue(), .reward = XRP(1), .minAccountCreate = XRP(20), .quorum = quorum, .signers = signers, .jvb = json::ValueType::Null}; initBridge(xrpB); // create USD -> USD bridge // ------------------------ BridgeDef usdB{ .doorA = doorUSDLocking, .issueA = usdLocking, .doorB = doorUSDIssuing, .issueB = usdIssuing, .reward = XRP(1), .minAccountCreate = XRP(20), .quorum = quorum, .signers = signers, .jvb = json::ValueType::Null}; initBridge(usdB); // 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, xrpB, {.from = a[0], .to = ua[0], .amt = XRP(777), .reward = xrpB.reward, .a2b = true}); xfer( 8, st, xrpB, {.from = a[0], .to = ua[0], .finaldest = a[2], .amt = XRP(3), .a2b = true}); runSimulation(st); // try the same thing in the other direction // ----------------------------------------- ac(0, st, xrpB, {.from = a[0], .to = ua[0], .amt = XRP(777), .reward = xrpB.reward, .a2b = false}); xfer( 8, st, xrpB, {.from = a[0], .to = ua[0], .finaldest = a[2], .amt = XRP(3), .a2b = false}); runSimulation(st); // run multiple XRP transfers // -------------------------- xfer( 0, st, xrpB, {.from = a[0], .to = a[0], .finaldest = a[1], .amt = XRP(6), .a2b = true, .withClaim = WithClaim::No}); xfer( 1, st, xrpB, {.from = a[0], .to = a[0], .finaldest = a[1], .amt = XRP(8), .a2b = false, .withClaim = WithClaim::No}); xfer( 1, st, xrpB, {.from = a[1], .to = a[1], .finaldest = a[1], .amt = XRP(1), .a2b = true}); xfer( 2, st, xrpB, {.from = a[0], .to = a[0], .finaldest = a[1], .amt = XRP(3), .a2b = false}); xfer( 2, st, xrpB, {.from = a[1], .to = a[1], .finaldest = a[1], .amt = XRP(5), .a2b = false}); xfer( 2, st, xrpB, {.from = a[0], .to = a[0], .finaldest = a[1], .amt = XRP(7), .a2b = false, .withClaim = WithClaim::No}); xfer( 2, st, xrpB, {.from = a[1], .to = a[1], .finaldest = a[1], .amt = XRP(9), .a2b = true}); runSimulation(st); // run one USD transfer // -------------------- xfer( 0, st, usdB, {.from = a[0], .to = a[1], .finaldest = a[2], .amt = usdLocking(3), .a2b = true}); runSimulation(st); // run multiple USD transfers // -------------------------- xfer( 0, st, usdB, {.from = a[0], .to = a[0], .finaldest = a[1], .amt = usdLocking(6), .a2b = true}); xfer( 1, st, usdB, {.from = a[0], .to = a[0], .finaldest = a[1], .amt = usdIssuing(8), .a2b = false}); xfer( 1, st, usdB, {.from = a[1], .to = a[1], .finaldest = a[1], .amt = usdLocking(1), .a2b = true}); xfer( 2, st, usdB, {.from = a[0], .to = a[0], .finaldest = a[1], .amt = usdIssuing(3), .a2b = false}); xfer( 2, st, usdB, {.from = a[1], .to = a[1], .finaldest = a[1], .amt = usdIssuing(5), .a2b = false}); xfer( 2, st, usdB, {.from = a[0], .to = a[0], .finaldest = a[1], .amt = usdIssuing(7), .a2b = false}); xfer( 2, st, usdB, {.from = a[1], .to = a[1], .finaldest = a[1], .amt = usdLocking(9), .a2b = true}); runSimulation(st); // run mixed transfers // ------------------- xfer( 0, st, xrpB, {.from = a[0], .to = a[0], .finaldest = a[0], .amt = XRP(1), .a2b = true}); xfer( 0, st, usdB, {.from = a[1], .to = a[3], .finaldest = a[3], .amt = usdIssuing(3), .a2b = false}); xfer( 0, st, usdB, {.from = a[3], .to = a[2], .finaldest = a[1], .amt = usdIssuing(5), .a2b = false}); xfer( 1, st, xrpB, {.from = a[0], .to = a[0], .finaldest = a[0], .amt = XRP(4), .a2b = false}); xfer( 1, st, xrpB, {.from = a[1], .to = a[1], .finaldest = a[0], .amt = XRP(8), .a2b = true}); xfer( 1, st, usdB, {.from = a[4], .to = a[1], .finaldest = a[1], .amt = usdLocking(7), .a2b = true}); xfer( 3, st, xrpB, {.from = a[1], .to = a[1], .finaldest = a[0], .amt = XRP(7), .a2b = true}); xfer( 3, st, xrpB, {.from = a[0], .to = a[4], .finaldest = a[3], .amt = XRP(2), .a2b = false}); xfer( 3, st, xrpB, {.from = a[1], .to = a[1], .finaldest = a[0], .amt = XRP(9), .a2b = true}); xfer( 3, st, usdB, {.from = a[3], .to = a[1], .finaldest = a[1], .amt = usdIssuing(11), .a2b = false}); runSimulation(st); // run multiple account create to stress attestation batching // ---------------------------------------------------------- ac(0, st, xrpB, {.from = a[0], .to = ua[1], .amt = XRP(301), .reward = xrpB.reward, .a2b = true}); ac(0, st, xrpB, {.from = a[1], .to = ua[2], .amt = XRP(302), .reward = xrpB.reward, .a2b = true}); ac(1, st, xrpB, {.from = a[0], .to = ua[3], .amt = XRP(303), .reward = xrpB.reward, .a2b = true}); ac(2, st, xrpB, {.from = a[1], .to = ua[4], .amt = XRP(304), .reward = xrpB.reward, .a2b = true}); ac(3, st, xrpB, {.from = a[0], .to = ua[5], .amt = XRP(305), .reward = xrpB.reward, .a2b = true}); ac(4, st, xrpB, {.from = a[1], .to = ua[6], .amt = XRP(306), .reward = xrpB.reward, .a2b = true}); ac(6, st, xrpB, {.from = a[0], .to = ua[7], .amt = XRP(307), .reward = xrpB.reward, .a2b = true}); ac(7, st, xrpB, {.from = a[2], .to = ua[8], .amt = XRP(308), .reward = xrpB.reward, .a2b = true}); ac(9, st, xrpB, {.from = a[0], .to = ua[9], .amt = XRP(309), .reward = xrpB.reward, .a2b = true}); ac(9, st, xrpB, {.from = a[0], .to = ua[9], .amt = XRP(309), .reward = xrpB.reward, .a2b = true}); ac(10, st, xrpB, {.from = a[0], .to = ua[10], .amt = XRP(310), .reward = xrpB.reward, .a2b = true}); ac(12, st, xrpB, {.from = a[0], .to = ua[11], .amt = XRP(311), .reward = xrpB.reward, .a2b = true}); ac(12, st, xrpB, {.from = a[3], .to = ua[12], .amt = XRP(312), .reward = xrpB.reward, .a2b = true}); ac(12, st, xrpB, {.from = a[4], .to = ua[13], .amt = XRP(313), .reward = xrpB.reward, .a2b = true}); ac(12, st, xrpB, {.from = a[3], .to = ua[14], .amt = XRP(314), .reward = xrpB.reward, .a2b = true}); ac(12, st, xrpB, {.from = a[6], .to = ua[15], .amt = XRP(315), .reward = xrpB.reward, .a2b = true}); ac(13, st, xrpB, {.from = a[7], .to = ua[16], .amt = XRP(316), .reward = xrpB.reward, .a2b = true}); ac(15, st, xrpB, {.from = a[3], .to = ua[17], .amt = XRP(317), .reward = xrpB.reward, .a2b = 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