#include #include #include #include #include // IWYU pragma: keep #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 { // Helper function that returns the reserve on an account based on // the passed in number of owners. static XRPAmount reserve(jtx::Env& env, std::uint32_t count) { return env.current()->fees().accountReserve(count); } // Helper function that returns true if acct has the lsfDepositAuth flag set. static bool hasDepositAuth(jtx::Env const& env, jtx::Account const& acct) { return env.le(acct)->isFlag(lsfDepositAuth); } struct DepositAuth_test : public beast::unit_test::Suite { void testEnable() { testcase("Enable"); using namespace jtx; Account const alice{"alice"}; { Env env(*this); env.fund(XRP(10000), alice); env(fset(alice, asfDepositAuth)); env.close(); BEAST_EXPECT(hasDepositAuth(env, alice)); env(fclear(alice, asfDepositAuth)); env.close(); BEAST_EXPECT(!hasDepositAuth(env, alice)); } } void testPayIOU() { // Exercise IOU payments and non-direct XRP payments to an account // that has the lsfDepositAuth flag set. testcase("Pay IOU"); using namespace jtx; Account const alice{"alice"}; Account const bob{"bob"}; Account const carol{"carol"}; Account const gw{"gw"}; IOU const usd = gw["USD"]; Env env(*this); env.fund(XRP(10000), alice, bob, carol, gw); env.close(); env.trust(usd(1000), alice, bob); env.close(); env(pay(gw, alice, usd(150))); env(offer(carol, usd(100), XRP(100))); env.close(); // Make sure bob's trust line is all set up so he can receive USD. env(pay(alice, bob, usd(50))); env.close(); // bob sets the lsfDepositAuth flag. env(fset(bob, asfDepositAuth), Require(Flags(bob, asfDepositAuth))); env.close(); // None of the following payments should succeed. auto failedIouPayments = [this, &env, &alice, &bob, &usd]() { env.require(Flags(bob, asfDepositAuth)); // Capture bob's balances before hand to confirm they don't change. PrettyAmount const bobXrpBalance{env.balance(bob, XRP)}; PrettyAmount const bobUsdBalance{env.balance(bob, usd)}; env(pay(alice, bob, usd(50)), Ter(tecNO_PERMISSION)); env.close(); // Note that even though alice is paying bob in XRP, the payment // is still not allowed since the payment passes through an offer. env(pay(alice, bob, drops(1)), Sendmax(usd(1)), Ter(tecNO_PERMISSION)); env.close(); BEAST_EXPECT(bobXrpBalance == env.balance(bob, XRP)); BEAST_EXPECT(bobUsdBalance == env.balance(bob, usd)); }; // Test when bob has an XRP balance > base reserve. failedIouPayments(); // Set bob's XRP balance == base reserve. Also demonstrate that // bob can make payments while his lsfDepositAuth flag is set. env(pay(bob, alice, usd(25))); env.close(); { STAmount const bobPaysXRP{env.balance(bob, XRP) - reserve(env, 1)}; XRPAmount const bobPaysFee{reserve(env, 1) - reserve(env, 0)}; env(pay(bob, alice, bobPaysXRP), Fee(bobPaysFee)); env.close(); } // Test when bob's XRP balance == base reserve. BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0)); BEAST_EXPECT(env.balance(bob, usd) == usd(25)); failedIouPayments(); // Test when bob has an XRP balance == 0. env(noop(bob), Fee(reserve(env, 0))); env.close(); BEAST_EXPECT(env.balance(bob, XRP) == XRP(0)); failedIouPayments(); // Give bob enough XRP for the fee to clear the lsfDepositAuth flag. env(pay(alice, bob, drops(env.current()->fees().base))); // bob clears the lsfDepositAuth and the next payment succeeds. env(fclear(bob, asfDepositAuth)); env.close(); env(pay(alice, bob, usd(50))); env.close(); env(pay(alice, bob, drops(1)), Sendmax(usd(1))); env.close(); } void testPayXRP() { // Exercise direct XRP payments to an account that has the // lsfDepositAuth flag set. testcase("Pay XRP"); using namespace jtx; Account const alice{"alice"}; Account const bob{"bob"}; Env env(*this); auto const baseFee = env.current()->fees().base; env.fund(XRP(10000), alice, bob); env.close(); // bob sets the lsfDepositAuth flag. env(fset(bob, asfDepositAuth), Fee(drops(baseFee))); env.close(); BEAST_EXPECT(env.balance(bob, XRP) == XRP(10000) - drops(baseFee)); // bob has more XRP than the base reserve. Any XRP payment should fail. env(pay(alice, bob, drops(1)), Ter(tecNO_PERMISSION)); env.close(); BEAST_EXPECT(env.balance(bob, XRP) == XRP(10000) - drops(baseFee)); // Change bob's XRP balance to exactly the base reserve. { STAmount const bobPaysXRP{env.balance(bob, XRP) - reserve(env, 1)}; XRPAmount const bobPaysFee{reserve(env, 1) - reserve(env, 0)}; env(pay(bob, alice, bobPaysXRP), Fee(bobPaysFee)); env.close(); } // bob has exactly the base reserve. A small enough direct XRP // payment should succeed. BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0)); env(pay(alice, bob, drops(1))); env.close(); // bob has exactly the base reserve + 1. No payment should succeed. BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0) + drops(1)); env(pay(alice, bob, drops(1)), Ter(tecNO_PERMISSION)); env.close(); // Take bob down to a balance of 0 XRP. env(noop(bob), Fee(reserve(env, 0) + drops(1))); env.close(); BEAST_EXPECT(env.balance(bob, XRP) == drops(0)); // We should not be able to pay bob more than the base reserve. env(pay(alice, bob, reserve(env, 0) + drops(1)), Ter(tecNO_PERMISSION)); env.close(); // However a payment of exactly the base reserve should succeed. env(pay(alice, bob, reserve(env, 0) + drops(0))); env.close(); BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0)); // We should be able to pay bob the base reserve one more time. env(pay(alice, bob, reserve(env, 0) + drops(0))); env.close(); BEAST_EXPECT(env.balance(bob, XRP) == (reserve(env, 0) + reserve(env, 0))); // bob's above the threshold again. Any payment should fail. env(pay(alice, bob, drops(1)), Ter(tecNO_PERMISSION)); env.close(); BEAST_EXPECT(env.balance(bob, XRP) == (reserve(env, 0) + reserve(env, 0))); // Take bob back down to a zero XRP balance. env(noop(bob), Fee(env.balance(bob, XRP))); env.close(); BEAST_EXPECT(env.balance(bob, XRP) == drops(0)); // bob should not be able to clear lsfDepositAuth. env(fclear(bob, asfDepositAuth), Ter(terINSUF_FEE_B)); env.close(); // We should be able to pay bob 1 drop now. env(pay(alice, bob, drops(1))); env.close(); BEAST_EXPECT(env.balance(bob, XRP) == drops(1)); // Pay bob enough so he can afford the fee to clear lsfDepositAuth. env(pay(alice, bob, drops(baseFee - 1))); env.close(); // Interestingly, at this point the terINSUF_FEE_B retry grabs the // request to clear lsfDepositAuth. So the balance should be zero // and lsfDepositAuth should be cleared. BEAST_EXPECT(env.balance(bob, XRP) == drops(0)); env.require(Nflags(bob, asfDepositAuth)); // Since bob no longer has lsfDepositAuth set we should be able to // pay him more than the base reserve. env(pay(alice, bob, reserve(env, 0) + drops(1))); env.close(); BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0) + drops(1)); } void testNoRipple() { // It its current incarnation the DepositAuth flag does not change // any behaviors regarding rippling and the NoRipple flag. // Demonstrate that. testcase("No Ripple"); using namespace jtx; Account const gw1("gw1"); Account const gw2("gw2"); Account const alice("alice"); Account const bob("bob"); IOU const usD1(gw1["USD"]); IOU const usD2(gw2["USD"]); auto testIssuer = [&](FeatureBitset const& features, bool noRipplePrev, bool noRippleNext, bool withDepositAuth) { Env env(*this, features); env.fund(XRP(10000), gw1, alice, bob); env.close(); env(trust(gw1, alice["USD"](10), noRipplePrev ? tfSetNoRipple : 0)); env(trust(gw1, bob["USD"](10), noRippleNext ? tfSetNoRipple : 0)); env.trust(usD1(10), alice, bob); env(pay(gw1, alice, usD1(10))); if (withDepositAuth) env(fset(gw1, asfDepositAuth)); TER const result = (noRippleNext && noRipplePrev) ? TER{tecPATH_DRY} : TER{tesSUCCESS}; env(pay(alice, bob, usD1(10)), Path(gw1), Ter(result)); }; auto testNonIssuer = [&](FeatureBitset const& features, bool noRipplePrev, bool noRippleNext, bool withDepositAuth) { Env env(*this, features); env.fund(XRP(10000), gw1, gw2, alice); env.close(); env(trust(alice, usD1(10), noRipplePrev ? tfSetNoRipple : 0)); env(trust(alice, usD2(10), noRippleNext ? tfSetNoRipple : 0)); env(pay(gw2, alice, usD2(10))); if (withDepositAuth) env(fset(alice, asfDepositAuth)); TER const result = (noRippleNext && noRipplePrev) ? TER{tecPATH_DRY} : TER{tesSUCCESS}; env(pay(gw1, gw2, usD2(10)), Path(alice), Sendmax(usD1(10)), Ter(result)); }; // Test every combo of noRipplePrev, noRippleNext, and withDepositAuth for (int i = 0; i < 8; ++i) { auto const noRipplePrev = i & 0x1; auto const noRippleNext = i & 0x2; auto const withDepositAuth = i & 0x4; testIssuer( testableAmendments(), noRipplePrev != 0, noRippleNext != 0, withDepositAuth != 0); testNonIssuer( testableAmendments(), noRipplePrev != 0, noRippleNext != 0, withDepositAuth != 0); } } void run() override { testEnable(); testPayIOU(); testPayXRP(); testNoRipple(); } }; static json::Value ledgerEntryDepositPreauth( jtx::Env& env, jtx::Account const& acc, std::vector const& auth) { json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::deposit_preauth][jss::owner] = acc.human(); jvParams[jss::deposit_preauth][jss::authorized_credentials] = json::ValueType::Array; auto& arr(jvParams[jss::deposit_preauth][jss::authorized_credentials]); for (auto const& o : auth) { arr.append(o.toLEJson()); } return env.rpc("json", "ledger_entry", to_string(jvParams)); } struct DepositPreauth_test : public beast::unit_test::Suite { void testEnable() { testcase("Enable"); using namespace jtx; Account const alice{"alice"}; Account const becky{"becky"}; { // o We should be able to add and remove an entry, and // o That entry should cost one reserve. // o The reserve should be returned when the entry is removed. Env env(*this); env.fund(XRP(10000), alice, becky); env.close(); // Add a DepositPreauth to alice. env(deposit::auth(alice, becky)); env.close(); env.require(Owners(alice, 1)); env.require(Owners(becky, 0)); // Remove a DepositPreauth from alice. env(deposit::unauth(alice, becky)); env.close(); env.require(Owners(alice, 0)); env.require(Owners(becky, 0)); } { // Verify that an account can be preauthorized and unauthorized // using tickets. Env env(*this); env.fund(XRP(10000), alice, becky); env.close(); env(ticket::create(alice, 2)); std::uint32_t const aliceSeq{env.seq(alice)}; env.close(); env.require(tickets(alice, 2)); // Consume the tickets from biggest seq to smallest 'cuz we can. std::uint32_t aliceTicketSeq{env.seq(alice)}; // Add a DepositPreauth to alice. env(deposit::auth(alice, becky), ticket::Use(--aliceTicketSeq)); env.close(); // Alice uses a ticket but gains a preauth entry. env.require(tickets(alice, 1)); env.require(Owners(alice, 2)); BEAST_EXPECT(env.seq(alice) == aliceSeq); env.require(Owners(becky, 0)); // Remove a DepositPreauth from alice. env(deposit::unauth(alice, becky), ticket::Use(--aliceTicketSeq)); env.close(); env.require(tickets(alice, 0)); env.require(Owners(alice, 0)); BEAST_EXPECT(env.seq(alice) == aliceSeq); env.require(Owners(becky, 0)); } } void testInvalid() { testcase("Invalid"); using namespace jtx; Account const alice{"alice"}; Account const becky{"becky"}; Account const carol{"carol"}; Env env(*this); // Tell env about alice, becky and carol since they are not yet funded. env.memoize(alice); env.memoize(becky); env.memoize(carol); // Add DepositPreauth to an unfunded account. env(deposit::auth(alice, becky), Seq(1), Ter(terNO_ACCOUNT)); env.fund(XRP(10000), alice, becky); env.close(); // Bad fee. env(deposit::auth(alice, becky), Fee(drops(-10)), Ter(temBAD_FEE)); env.close(); // Bad flags. env(deposit::auth(alice, becky), Txflags(tfSell), Ter(temINVALID_FLAG)); env.close(); { // Neither auth not unauth. json::Value tx{deposit::auth(alice, becky)}; tx.removeMember(sfAuthorize.jsonName); env(tx, Ter(temMALFORMED)); env.close(); } { // Both auth and unauth. json::Value tx{deposit::auth(alice, becky)}; tx[sfUnauthorize.jsonName] = becky.human(); env(tx, Ter(temMALFORMED)); env.close(); } { // Alice authorizes a zero account. json::Value tx{deposit::auth(alice, becky)}; tx[sfAuthorize.jsonName] = to_string(xrpAccount()); env(tx, Ter(temINVALID_ACCOUNT_ID)); env.close(); } // alice authorizes herself. env(deposit::auth(alice, alice), Ter(temCANNOT_PREAUTH_SELF)); env.close(); // alice authorizes an unfunded account. env(deposit::auth(alice, carol), Ter(tecNO_TARGET)); env.close(); // alice successfully authorizes becky. env.require(Owners(alice, 0)); env.require(Owners(becky, 0)); env(deposit::auth(alice, becky)); env.close(); env.require(Owners(alice, 1)); env.require(Owners(becky, 0)); // alice attempts to create a duplicate authorization. env(deposit::auth(alice, becky), Ter(tecDUPLICATE)); env.close(); env.require(Owners(alice, 1)); env.require(Owners(becky, 0)); // carol attempts to preauthorize but doesn't have enough reserve. env.fund(drops(249'999'999), carol); env.close(); env(deposit::auth(carol, becky), Ter(tecINSUFFICIENT_RESERVE)); env.close(); env.require(Owners(carol, 0)); env.require(Owners(becky, 0)); // carol gets enough XRP to (barely) meet the reserve. env(pay(alice, carol, drops(env.current()->fees().base + 1))); env.close(); env(deposit::auth(carol, becky)); env.close(); env.require(Owners(carol, 1)); env.require(Owners(becky, 0)); // But carol can't meet the reserve for another pre-authorization. env(deposit::auth(carol, alice), Ter(tecINSUFFICIENT_RESERVE)); env.close(); env.require(Owners(carol, 1)); env.require(Owners(becky, 0)); env.require(Owners(alice, 1)); // alice attempts to remove an authorization she doesn't have. env(deposit::unauth(alice, carol), Ter(tecNO_ENTRY)); env.close(); env.require(Owners(alice, 1)); env.require(Owners(becky, 0)); // alice successfully removes her authorization of becky. env(deposit::unauth(alice, becky)); env.close(); env.require(Owners(alice, 0)); env.require(Owners(becky, 0)); // alice removes becky again and gets an error. env(deposit::unauth(alice, becky), Ter(tecNO_ENTRY)); env.close(); env.require(Owners(alice, 0)); env.require(Owners(becky, 0)); } void testPayment(FeatureBitset features) { testcase("Payment"); using namespace jtx; Account const alice{"alice"}; Account const becky{"becky"}; Account const gw{"gw"}; IOU const usd(gw["USD"]); { // The initial implementation of DepositAuth had a bug where an // account with the DepositAuth flag set could not make a payment // to itself. That bug was fixed in the DepositPreauth amendment. Env env(*this, features); env.fund(XRP(5000), alice, becky, gw); env.close(); env.trust(usd(1000), alice); env.trust(usd(1000), becky); env.close(); env(pay(gw, alice, usd(500))); env.close(); env(offer(alice, XRP(100), usd(100), tfPassive), Require(offers(alice, 1))); env.close(); // becky pays herself USD (10) by consuming part of alice's offer. // Make sure the payment works if PaymentAuth is not involved. env(pay(becky, becky, usd(10)), Path(~usd), Sendmax(XRP(10))); env.close(); // becky decides to require authorization for deposits. env(fset(becky, asfDepositAuth)); env.close(); // becky pays herself again. env(pay(becky, becky, usd(10)), Path(~usd), Sendmax(XRP(10)), Ter(tesSUCCESS)); env.close(); { // becky setup depositpreauth with credentials char const credType[] = "abcde"; Account const carol{"carol"}; env.fund(XRP(5000), carol); env.close(); bool const supportsCredentials = features[featureCredentials]; TER const expectTer(!supportsCredentials ? TER(temDISABLED) : TER(tesSUCCESS)); env(deposit::authCredentials(becky, {{carol, credType}}), Ter(expectTer)); env.close(); // gw accept credentials env(credentials::create(gw, carol, credType), Ter(expectTer)); env.close(); env(credentials::accept(gw, carol, credType), Ter(expectTer)); env.close(); auto jv = credentials::ledgerEntry(env, gw, carol, credType); std::string const credIdx = supportsCredentials ? jv[jss::result][jss::index].asString() : "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6" "EA288BE4"; env(pay(gw, becky, usd(100)), credentials::Ids({credIdx}), Ter(expectTer)); env.close(); } } // Make sure DepositPreauthorization works for payments. Account const carol{"carol"}; Env env(*this, features); env.fund(XRP(5000), alice, becky, carol, gw); env.close(); env.trust(usd(1000), alice); env.trust(usd(1000), becky); env.trust(usd(1000), carol); env.close(); env(pay(gw, alice, usd(1000))); env.close(); // Make XRP and IOU payments from alice to becky. Should be fine. env(pay(alice, becky, XRP(100))); env(pay(alice, becky, usd(100))); env.close(); // becky decides to require authorization for deposits. env(fset(becky, asfDepositAuth)); env.close(); // alice can no longer pay becky. env(pay(alice, becky, XRP(100)), Ter(tecNO_PERMISSION)); env(pay(alice, becky, usd(100)), Ter(tecNO_PERMISSION)); env.close(); // becky preauthorizes carol for deposit, which doesn't provide // authorization for alice. env(deposit::auth(becky, carol)); env.close(); // alice still can't pay becky. env(pay(alice, becky, XRP(100)), Ter(tecNO_PERMISSION)); env(pay(alice, becky, usd(100)), Ter(tecNO_PERMISSION)); env.close(); // becky preauthorizes alice for deposit. env(deposit::auth(becky, alice)); env.close(); // alice can now pay becky. env(pay(alice, becky, XRP(100))); env(pay(alice, becky, usd(100))); env.close(); // alice decides to require authorization for deposits. env(fset(alice, asfDepositAuth)); env.close(); // Even though alice is authorized to pay becky, becky is not // authorized to pay alice. env(pay(becky, alice, XRP(100)), Ter(tecNO_PERMISSION)); env(pay(becky, alice, usd(100)), Ter(tecNO_PERMISSION)); env.close(); // becky unauthorizes carol. Should have no impact on alice. env(deposit::unauth(becky, carol)); env.close(); env(pay(alice, becky, XRP(100))); env(pay(alice, becky, usd(100))); env.close(); // becky unauthorizes alice. alice now can't pay becky. env(deposit::unauth(becky, alice)); env.close(); env(pay(alice, becky, XRP(100)), Ter(tecNO_PERMISSION)); env(pay(alice, becky, usd(100)), Ter(tecNO_PERMISSION)); env.close(); // becky decides to remove authorization for deposits. Now // alice can pay becky again. env(fclear(becky, asfDepositAuth)); env.close(); env(pay(alice, becky, XRP(100))); env(pay(alice, becky, usd(100))); env.close(); } void testCredentialsPayment() { using namespace jtx; char const credType[] = "abcde"; Account const issuer{"issuer"}; Account const alice{"alice"}; Account const bob{"bob"}; Account const maria{"maria"}; Account const john{"john"}; { testcase("Payment failure with disabled credentials rule."); Env env(*this, testableAmendments() - featureCredentials); env.fund(XRP(5000), issuer, bob, alice); env.close(); // Bob require pre-authorization env(fset(bob, asfDepositAuth)); env.close(); // Setup DepositPreauth object failed - amendent is not supported env(deposit::authCredentials(bob, {{issuer, credType}}), Ter(temDISABLED)); env.close(); // But can create old DepositPreauth env(deposit::auth(bob, alice)); env.close(); // And alice can't pay with any credentials, amendment is not // enabled std::string const invalidIdx = "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" "01E034"; env(pay(alice, bob, XRP(10)), credentials::Ids({invalidIdx}), Ter(temDISABLED)); env.close(); } { testcase("Payment with credentials."); Env env(*this); env.fund(XRP(5000), issuer, alice, bob, john); env.close(); // Issuer create credentials, but Alice didn't accept them yet env(credentials::create(alice, issuer, credType)); env.close(); // Get the index of the credentials auto const jv = credentials::ledgerEntry(env, alice, issuer, credType); std::string const credIdx = jv[jss::result][jss::index].asString(); // Bob require pre-authorization env(fset(bob, asfDepositAuth)); env.close(); // Bob will accept payments from accounts with credentials signed // by 'issuer' env(deposit::authCredentials(bob, {{issuer, credType}})); env.close(); auto const jDP = ledgerEntryDepositPreauth(env, bob, {{issuer, credType}}); BEAST_EXPECT( jDP.isObject() && jDP.isMember(jss::result) && !jDP[jss::result].isMember(jss::error) && jDP[jss::result].isMember(jss::node) && jDP[jss::result][jss::node].isMember("LedgerEntryType") && jDP[jss::result][jss::node]["LedgerEntryType"] == jss::DepositPreauth); // Alice can't pay - empty credentials array { auto jv = pay(alice, bob, XRP(100)); jv[sfCredentialIDs.jsonName] = json::ValueType::Array; env(jv, Ter(temMALFORMED)); env.close(); } // Alice can't pay - not accepted credentials env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx}), Ter(tecBAD_CREDENTIALS)); env.close(); // Alice accept the credentials env(credentials::accept(alice, issuer, credType)); env.close(); // Now Alice can pay env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx})); env.close(); // Alice can pay Maria without depositPreauth enabled env(pay(alice, maria, XRP(250)), credentials::Ids({credIdx})); env.close(); // john can accept payment with old depositPreauth and valid // credentials env(fset(john, asfDepositAuth)); env(deposit::auth(john, alice)); env(pay(alice, john, XRP(100)), credentials::Ids({credIdx})); env.close(); } { testcase("Payment failure with invalid credentials."); Env env(*this); env.fund(XRP(10000), issuer, alice, bob, maria); env.close(); // Issuer create credentials, but Alice didn't accept them yet env(credentials::create(alice, issuer, credType)); env.close(); // Alice accept the credentials env(credentials::accept(alice, issuer, credType)); env.close(); // Get the index of the credentials auto const jv = credentials::ledgerEntry(env, alice, issuer, credType); std::string const credIdx = jv[jss::result][jss::index].asString(); { // Success as destination didn't enable pre-authorization so // valid credentials will not fail env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx})); } // Bob require pre-authorization env(fset(bob, asfDepositAuth)); env.close(); { // Fail as destination didn't setup DepositPreauth object env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx}), Ter(tecNO_PERMISSION)); } // Bob setup DepositPreauth object, duplicates is not allowed env(deposit::authCredentials(bob, {{issuer, credType}, {issuer, credType}}), Ter(temMALFORMED)); // Bob setup DepositPreauth object env(deposit::authCredentials(bob, {{issuer, credType}})); env.close(); { std::string const invalidIdx = "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" "01E034"; // Alice can't pay with non-existing credentials env(pay(alice, bob, XRP(100)), credentials::Ids({invalidIdx}), Ter(tecBAD_CREDENTIALS)); } { // maria can't pay using valid credentials but issued for // different account env(pay(maria, bob, XRP(100)), credentials::Ids({credIdx}), Ter(tecBAD_CREDENTIALS)); } { // create another valid credential char const credType2[] = "fghij"; env(credentials::create(alice, issuer, credType2)); env.close(); env(credentials::accept(alice, issuer, credType2)); env.close(); auto const jv = credentials::ledgerEntry(env, alice, issuer, credType2); std::string const credIdx2 = jv[jss::result][jss::index].asString(); // Alice can't pay with invalid set of valid credentials env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx, credIdx2}), Ter(tecNO_PERMISSION)); } // Error, duplicate credentials env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx, credIdx}), Ter(temMALFORMED)); // Alice can pay env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx})); env.close(); } } void testCredentialsCreation() { using namespace jtx; char const credType[] = "abcde"; Account const issuer{"issuer"}; Account const alice{"alice"}; Account const bob{"bob"}; Account const maria{"maria"}; { testcase("Creating / deleting with credentials."); Env env(*this); env.fund(XRP(5000), issuer, alice, bob); env.close(); { // both included [AuthorizeCredentials UnauthorizeCredentials] auto jv = deposit::authCredentials(bob, {{issuer, credType}}); jv[sfUnauthorizeCredentials.jsonName] = json::ValueType::Array; env(jv, Ter(temMALFORMED)); } { // both included [Unauthorize, AuthorizeCredentials] auto jv = deposit::authCredentials(bob, {{issuer, credType}}); jv[sfUnauthorize.jsonName] = issuer.human(); env(jv, Ter(temMALFORMED)); } { // both included [Authorize, AuthorizeCredentials] auto jv = deposit::authCredentials(bob, {{issuer, credType}}); jv[sfAuthorize.jsonName] = issuer.human(); env(jv, Ter(temMALFORMED)); } { // both included [Unauthorize, UnauthorizeCredentials] auto jv = deposit::unauthCredentials(bob, {{issuer, credType}}); jv[sfUnauthorize.jsonName] = issuer.human(); env(jv, Ter(temMALFORMED)); } { // both included [Authorize, UnauthorizeCredentials] auto jv = deposit::unauthCredentials(bob, {{issuer, credType}}); jv[sfAuthorize.jsonName] = issuer.human(); env(jv, Ter(temMALFORMED)); } { // AuthorizeCredentials is empty auto jv = deposit::authCredentials(bob, {}); env(jv, Ter(temARRAY_EMPTY)); } { // invalid issuer auto jv = deposit::authCredentials(bob, {}); auto& arr(jv[sfAuthorizeCredentials.jsonName]); json::Value cred = json::ValueType::Object; cred[jss::Issuer] = to_string(xrpAccount()); cred[sfCredentialType.jsonName] = strHex(std::string_view(credType)); json::Value credParent; credParent[jss::Credential] = cred; arr.append(std::move(credParent)); env(jv, Ter(temINVALID_ACCOUNT_ID)); } { // empty credential type auto jv = deposit::authCredentials(bob, {{issuer, {}}}); env(jv, Ter(temMALFORMED)); } { // AuthorizeCredentials is larger than 8 elements Account const a("a"), b("b"), c("c"), d("d"), e("e"), f("f"), g("g"), h("h"), i("i"); auto const& z = credType; auto jv = deposit::authCredentials( bob, {{a, z}, {b, z}, {c, z}, {d, z}, {e, z}, {f, z}, {g, z}, {h, z}, {i, z}}); env(jv, Ter(temARRAY_TOO_LARGE)); } { // Can't create with non-existing issuer Account const rick{"rick"}; auto jv = deposit::authCredentials(bob, {{rick, credType}}); env(jv, Ter(tecNO_ISSUER)); env.close(); } { // not enough reserve Account const john{"john"}; env.fund(env.current()->fees().accountReserve(0), john); env.close(); auto jv = deposit::authCredentials(john, {{issuer, credType}}); env(jv, Ter(tecINSUFFICIENT_RESERVE)); } { // NO deposit object exists env(deposit::unauthCredentials(bob, {{issuer, credType}}), Ter(tecNO_ENTRY)); } // Create DepositPreauth object { env(deposit::authCredentials(bob, {{issuer, credType}})); env.close(); auto const jDP = ledgerEntryDepositPreauth(env, bob, {{issuer, credType}}); BEAST_EXPECT( jDP.isObject() && jDP.isMember(jss::result) && !jDP[jss::result].isMember(jss::error) && jDP[jss::result].isMember(jss::node) && jDP[jss::result][jss::node].isMember("LedgerEntryType") && jDP[jss::result][jss::node]["LedgerEntryType"] == jss::DepositPreauth); // Check object fields BEAST_EXPECT(jDP[jss::result][jss::node][jss::Account] == bob.human()); auto const& credentials(jDP[jss::result][jss::node]["AuthorizeCredentials"]); BEAST_EXPECT(credentials.isArray() && credentials.size() == 1); for (auto const& o : credentials) { auto const& c(o[jss::Credential]); BEAST_EXPECT(c[jss::Issuer].asString() == issuer.human()); BEAST_EXPECT( c["CredentialType"].asString() == strHex(std::string_view(credType))); } // can't create duplicate env(deposit::authCredentials(bob, {{issuer, credType}}), Ter(tecDUPLICATE)); } // Delete DepositPreauth object { env(deposit::unauthCredentials(bob, {{issuer, credType}})); env.close(); auto const jDP = ledgerEntryDepositPreauth(env, bob, {{issuer, credType}}); BEAST_EXPECT( jDP.isObject() && jDP.isMember(jss::result) && jDP[jss::result].isMember(jss::error) && jDP[jss::result][jss::error] == "entryNotFound"); } } } void testExpiredCreds() { using namespace jtx; char const credType[] = "abcde"; char const credType2[] = "fghijkl"; Account const issuer{"issuer"}; Account const alice{"alice"}; Account const bob{"bob"}; Account const gw{"gw"}; IOU const usd = gw["USD"]; Account const zelda{"zelda"}; { testcase("Payment failure with expired credentials."); Env env(*this); env.fund(XRP(10000), issuer, alice, bob, gw); env.close(); // Create credentials auto jv = credentials::create(alice, issuer, credType); // Current time in XRPL epoch. // Every time ledger close, unittest timer increase by 10s uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count() + 60; jv[sfExpiration.jsonName] = t; env(jv); env.close(); // Alice accept the credentials env(credentials::accept(alice, issuer, credType)); env.close(); // Create credential which not expired jv = credentials::create(alice, issuer, credType2); uint32_t const t2 = env.current()->header().parentCloseTime.time_since_epoch().count() + 1000; jv[sfExpiration.jsonName] = t2; env(jv); env.close(); env(credentials::accept(alice, issuer, credType2)); env.close(); BEAST_EXPECT(ownerCount(env, issuer) == 0); BEAST_EXPECT(ownerCount(env, alice) == 2); // Get the index of the credentials jv = credentials::ledgerEntry(env, alice, issuer, credType); std::string const credIdx = jv[jss::result][jss::index].asString(); jv = credentials::ledgerEntry(env, alice, issuer, credType2); std::string const credIdx2 = jv[jss::result][jss::index].asString(); // Bob require pre-authorization env(fset(bob, asfDepositAuth)); env.close(); // Bob setup DepositPreauth object env(deposit::authCredentials(bob, {{issuer, credType}, {issuer, credType2}})); env.close(); { // Alice can pay env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx, credIdx2})); env.close(); env.close(); // Ledger closed, time increased, alice can't pay anymore env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx, credIdx2}), Ter(tecEXPIRED)); env.close(); { // check that expired credentials were deleted auto const jDelCred = credentials::ledgerEntry(env, alice, issuer, credType); BEAST_EXPECT( jDelCred.isObject() && jDelCred.isMember(jss::result) && jDelCred[jss::result].isMember(jss::error) && jDelCred[jss::result][jss::error] == "entryNotFound"); } { // check that non-expired credential still present auto const jle = credentials::ledgerEntry(env, alice, issuer, credType2); BEAST_EXPECT( jle.isObject() && jle.isMember(jss::result) && !jle[jss::result].isMember(jss::error) && jle[jss::result].isMember(jss::node) && jle[jss::result][jss::node].isMember("LedgerEntryType") && jle[jss::result][jss::node]["LedgerEntryType"] == jss::Credential && jle[jss::result][jss::node][jss::Issuer] == issuer.human() && jle[jss::result][jss::node][jss::Subject] == alice.human() && jle[jss::result][jss::node]["CredentialType"] == strHex(std::string_view(credType2))); } BEAST_EXPECT(ownerCount(env, issuer) == 0); BEAST_EXPECT(ownerCount(env, alice) == 1); } { auto jv = credentials::create(gw, issuer, credType); uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count() + 40; jv[sfExpiration.jsonName] = t; env(jv); env.close(); env(credentials::accept(gw, issuer, credType)); env.close(); jv = credentials::ledgerEntry(env, gw, issuer, credType); std::string const credIdx = jv[jss::result][jss::index].asString(); BEAST_EXPECT(ownerCount(env, issuer) == 0); BEAST_EXPECT(ownerCount(env, gw) == 1); env.close(); env.close(); env.close(); // credentials are expired env(pay(gw, bob, usd(150)), credentials::Ids({credIdx}), Ter(tecEXPIRED)); env.close(); // check that expired credentials were deleted auto const jDelCred = credentials::ledgerEntry(env, gw, issuer, credType); BEAST_EXPECT( jDelCred.isObject() && jDelCred.isMember(jss::result) && jDelCred[jss::result].isMember(jss::error) && jDelCred[jss::result][jss::error] == "entryNotFound"); BEAST_EXPECT(ownerCount(env, issuer) == 0); BEAST_EXPECT(ownerCount(env, gw) == 0); } } { using namespace std::chrono; testcase("Escrow failure with expired credentials."); Env env(*this); env.fund(XRP(5000), issuer, alice, bob, zelda); env.close(); // Create credentials auto jv = credentials::create(zelda, issuer, credType); uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count() + 50; jv[sfExpiration.jsonName] = t; env(jv); env.close(); // Zelda accept the credentials env(credentials::accept(zelda, issuer, credType)); env.close(); // Get the index of the credentials jv = credentials::ledgerEntry(env, zelda, issuer, credType); std::string const credIdx = jv[jss::result][jss::index].asString(); // Bob require pre-authorization env(fset(bob, asfDepositAuth)); env.close(); // Bob setup DepositPreauth object env(deposit::authCredentials(bob, {{issuer, credType}})); env.close(); auto const seq = env.seq(alice); env(escrow::create(alice, bob, XRP(1000)), escrow::kFINISH_TIME(env.now() + 1s)); env.close(); // zelda can't finish escrow with invalid credentials { env(escrow::finish(zelda, alice, seq), credentials::Ids({}), Ter(temMALFORMED)); env.close(); } { // zelda can't finish escrow with invalid credentials std::string const invalidIdx = "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" "01E034"; env(escrow::finish(zelda, alice, seq), credentials::Ids({invalidIdx}), Ter(tecBAD_CREDENTIALS)); env.close(); } { // Ledger closed, time increased, zelda can't finish escrow env(escrow::finish(zelda, alice, seq), credentials::Ids({credIdx}), Fee(1500), Ter(tecEXPIRED)); env.close(); } // check that expired credentials were deleted auto const jDelCred = credentials::ledgerEntry(env, zelda, issuer, credType); BEAST_EXPECT( jDelCred.isObject() && jDelCred.isMember(jss::result) && jDelCred[jss::result].isMember(jss::error) && jDelCred[jss::result][jss::error] == "entryNotFound"); } } void testSortingCredentials() { using namespace jtx; Account const stock{"stock"}; Account const alice{"alice"}; Account const bob{"bob"}; Env env(*this); testcase("Sorting credentials."); env.fund(XRP(5000), stock, alice, bob); std::vector credentials = { {"a", "a"}, {"b", "b"}, {"c", "c"}, {"d", "d"}, {"e", "e"}, {"f", "f"}, {"g", "g"}, {"h", "h"}}; for (auto const& c : credentials) env.fund(XRP(5000), c.issuer); env.close(); std::random_device rd; std::mt19937 gen(rd()); { std::unordered_map pubKey2Acc; for (auto const& c : credentials) pubKey2Acc.emplace(c.issuer.human(), c.issuer); // check sorting in object for (int i = 0; i < 10; ++i) { std::ranges::shuffle(credentials, gen); env(deposit::authCredentials(stock, credentials)); env.close(); auto const dp = ledgerEntryDepositPreauth(env, stock, credentials); auto const& authCred(dp[jss::result][jss::node]["AuthorizeCredentials"]); BEAST_EXPECT(authCred.isArray() && authCred.size() == credentials.size()); std::vector> readCreds; for (auto const& o : authCred) { auto const& c(o[jss::Credential]); auto issuer = c[jss::Issuer].asString(); if (BEAST_EXPECT(pubKey2Acc.contains(issuer))) { readCreds.emplace_back( pubKey2Acc.at(issuer), c["CredentialType"].asString()); } } BEAST_EXPECT(std::ranges::is_sorted(readCreds)); env(deposit::unauthCredentials(stock, credentials)); env.close(); } } { std::ranges::shuffle(credentials, gen); env(deposit::authCredentials(stock, credentials)); env.close(); // check sorting in params for (int i = 0; i < 10; ++i) { std::ranges::shuffle(credentials, gen); env(deposit::authCredentials(stock, credentials), Ter(tecDUPLICATE)); } } testcase("Check duplicate credentials."); { // check duplicates in depositPreauth params std::vector copyCredentials( credentials.begin(), credentials.end() - 1); std::ranges::shuffle(copyCredentials, gen); for (auto const& c : copyCredentials) { auto credentials2 = copyCredentials; credentials2.push_back(c); env(deposit::authCredentials(stock, credentials2), Ter(temMALFORMED)); } // create batch of credentials and save their hashes std::vector credentialIDs; for (auto const& c : credentials) { env(credentials::create(alice, c.issuer, c.credType)); env.close(); env(credentials::accept(alice, c.issuer, c.credType)); env.close(); credentialIDs.push_back( credentials::ledgerEntry( env, alice, c.issuer, c.credType)[jss::result][jss::index] .asString()); } // check duplicates in payment params for (auto const& h : credentialIDs) { auto credentialIDs2 = credentialIDs; credentialIDs2.push_back(h); env(pay(alice, bob, XRP(100)), credentials::Ids(credentialIDs2), Ter(temMALFORMED)); } } } void run() override { testEnable(); testInvalid(); auto const supported{jtx::testableAmendments()}; testPayment(supported - featureCredentials); testPayment(supported); testCredentialsPayment(); testCredentialsCreation(); testExpiredCreds(); testSortingCredentials(); } }; BEAST_DEFINE_TESTSUITE(DepositAuth, app, xrpl); BEAST_DEFINE_TESTSUITE(DepositPreauth, app, xrpl); } // namespace xrpl::test