#include #include #include #include #include #include #include #include namespace xrpl { namespace test { using namespace jtx::paychan; struct PayChan_test : public beast::unit_test::suite { static std::pair> channelKeyAndSle(ReadView const& view, jtx::Account const& account, jtx::Account const& dst) { auto const sle = view.read(keylet::account(account)); if (!sle) return {}; auto const k = keylet::payChan(account, dst, (*sle)[sfSequence] - 1); return {k.key, view.read(k)}; } static Buffer signClaimAuth( PublicKey const& pk, SecretKey const& sk, uint256 const& channel, STAmount const& authAmt) { Serializer msg; serializePayChanAuthorization(msg, channel, authAmt.xrp()); return sign(pk, sk, msg.slice()); } static STAmount channelAmount(ReadView const& view, uint256 const& chan) { auto const slep = view.read({ltPAYCHAN, chan}); if (!slep) return XRPAmount{-1}; return (*slep)[sfAmount]; } static std::optional channelExpiration(ReadView const& view, uint256 const& chan) { auto const slep = view.read({ltPAYCHAN, chan}); if (!slep) return std::nullopt; if (auto const r = (*slep)[~sfExpiration]) return r.value(); return std::nullopt; } void testSimple(FeatureBitset features) { testcase("simple"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); auto USDA = alice["USD"]; env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 100s; auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk)); BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); { auto const preAlice = env.balance(alice); env(fund(alice, chan, XRP(1000))); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops); } auto chanBal = channelBalance(*env.current(), chan); auto chanAmt = channelAmount(*env.current(), chan); BEAST_EXPECT(chanBal == XRP(0)); BEAST_EXPECT(chanAmt == XRP(2000)); { // bad amounts (non-xrp, negative amounts) env(create(alice, bob, USDA(1000), settleDelay, pk), ter(temBAD_AMOUNT)); env(fund(alice, chan, USDA(1000)), ter(temBAD_AMOUNT)); env(create(alice, bob, XRP(-1000), settleDelay, pk), ter(temBAD_AMOUNT)); env(fund(alice, chan, XRP(-1000)), ter(temBAD_AMOUNT)); } // invalid account env(create(alice, "noAccount", XRP(1000), settleDelay, pk), ter(tecNO_DST)); // can't create channel to the same account env(create(alice, alice, XRP(1000), settleDelay, pk), ter(temDST_IS_SRC)); // invalid channel env(fund(alice, channel(alice, "noAccount", env.seq(alice) - 1), XRP(1000)), ter(tecNO_ENTRY)); // not enough funds env(create(alice, bob, XRP(10000), settleDelay, pk), ter(tecUNFUNDED)); { // No signature claim with bad amounts (negative and non-xrp) auto const iou = USDA(100).value(); auto const negXRP = XRP(-100).value(); auto const posXRP = XRP(100).value(); env(claim(alice, chan, iou, iou), ter(temBAD_AMOUNT)); env(claim(alice, chan, posXRP, iou), ter(temBAD_AMOUNT)); env(claim(alice, chan, iou, posXRP), ter(temBAD_AMOUNT)); env(claim(alice, chan, negXRP, negXRP), ter(temBAD_AMOUNT)); env(claim(alice, chan, posXRP, negXRP), ter(temBAD_AMOUNT)); env(claim(alice, chan, negXRP, posXRP), ter(temBAD_AMOUNT)); } { // No signature claim more than authorized auto const delta = XRP(500); auto const reqBal = chanBal + delta; auto const authAmt = reqBal + XRP(-100); assert(reqBal <= chanAmt); env(claim(alice, chan, reqBal, authAmt), ter(temBAD_AMOUNT)); } { // No signature needed since the owner is claiming auto const preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; auto const authAmt = reqBal + XRP(100); assert(reqBal <= chanAmt); env(claim(alice, chan, reqBal, authAmt)); BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob + delta); chanBal = reqBal; } { // Claim with signature auto preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; auto const authAmt = reqBal + XRP(100); assert(reqBal <= chanAmt); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops); chanBal = reqBal; // claim again preBob = env.balance(bob); env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), ter(tecUNFUNDED_PAYMENT)); BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); } { // Try to claim more than authorized auto const preBob = env.balance(bob); STAmount const authAmt = chanBal + XRP(500); STAmount const reqAmt = authAmt + STAmount{1}; assert(reqAmt <= chanAmt); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); env(claim(bob, chan, reqAmt, authAmt, Slice(sig), alice.pk()), ter(temBAD_AMOUNT)); BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob); } // Dst tries to fund the channel env(fund(bob, chan, XRP(1000)), ter(tecNO_PERMISSION)); BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); { // Wrong signing key auto const sig = signClaimAuth(bob.pk(), bob.sk(), chan, XRP(1500)); env(claim(bob, chan, XRP(1500).value(), XRP(1500).value(), Slice(sig), bob.pk()), ter(temBAD_SIGNER)); BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); } { // Bad signature auto const sig = signClaimAuth(bob.pk(), bob.sk(), chan, XRP(1500)); env(claim(bob, chan, XRP(1500).value(), XRP(1500).value(), Slice(sig), alice.pk()), ter(temBAD_SIGNATURE)); BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); } { // Dst closes channel auto const preAlice = env.balance(alice); auto const preBob = env.balance(bob); env(claim(bob, chan), txflags(tfClose)); BEAST_EXPECT(!channelExists(*env.current(), chan)); auto const feeDrops = env.current()->fees().base; auto const delta = chanAmt - chanBal; assert(delta > beast::zero); BEAST_EXPECT(env.balance(alice) == preAlice + delta); BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); } } void testDisallowIncoming(FeatureBitset features) { testcase("Disallow Incoming Flag"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const cho = Account("cho"); env.fund(XRP(10000), alice, bob, cho); auto const pk = alice.pk(); auto const settleDelay = 100s; // set flag on bob only env(fset(bob, asfDisallowIncomingPayChan)); env.close(); // channel creation from alice to bob is disallowed { auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk), ter(tecNO_PERMISSION)); BEAST_EXPECT(!channelExists(*env.current(), chan)); } // set flag on alice also env(fset(alice, asfDisallowIncomingPayChan)); env.close(); // channel creation from bob to alice is now disallowed { auto const chan = channel(bob, alice, env.seq(bob)); env(create(bob, alice, XRP(1000), settleDelay, pk), ter(tecNO_PERMISSION)); BEAST_EXPECT(!channelExists(*env.current(), chan)); } // remove flag from bob env(fclear(bob, asfDisallowIncomingPayChan)); env.close(); // now the channel between alice and bob can exist { auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk), ter(tesSUCCESS)); BEAST_EXPECT(channelExists(*env.current(), chan)); } // a channel from cho to alice isn't allowed { auto const chan = channel(cho, alice, env.seq(cho)); env(create(cho, alice, XRP(1000), settleDelay, pk), ter(tecNO_PERMISSION)); BEAST_EXPECT(!channelExists(*env.current(), chan)); } // remove flag from alice env(fclear(alice, asfDisallowIncomingPayChan)); env.close(); // now a channel from cho to alice is allowed { auto const chan = channel(cho, alice, env.seq(cho)); env(create(cho, alice, XRP(1000), settleDelay, pk), ter(tesSUCCESS)); BEAST_EXPECT(channelExists(*env.current(), chan)); } } void testCancelAfter(FeatureBitset features) { testcase("cancel after"); using namespace jtx; using namespace std::literals::chrono_literals; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const carol = Account("carol"); { // If dst claims after cancel after, channel closes Env env{*this, features}; env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 100s; NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime + 3600s; auto const channelFunds = XRP(1000); auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); BEAST_EXPECT(channelExists(*env.current(), chan)); env.close(cancelAfter); { // dst cannot claim after cancelAfter auto const chanBal = channelBalance(*env.current(), chan); auto const chanAmt = channelAmount(*env.current(), chan); auto preAlice = env.balance(alice); auto preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; auto const authAmt = reqBal + XRP(100); assert(reqBal <= chanAmt); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(!channelExists(*env.current(), chan)); BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); BEAST_EXPECT(env.balance(alice) == preAlice + channelFunds); } } { // Third party can close after cancel after Env env{*this, features}; env.fund(XRP(10000), alice, bob, carol); auto const pk = alice.pk(); auto const settleDelay = 100s; NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime + 3600s; auto const channelFunds = XRP(1000); auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); BEAST_EXPECT(channelExists(*env.current(), chan)); // third party close before cancelAfter env(claim(carol, chan), txflags(tfClose), ter(tecNO_PERMISSION)); BEAST_EXPECT(channelExists(*env.current(), chan)); env.close(cancelAfter); // third party close after cancelAfter auto const preAlice = env.balance(alice); env(claim(carol, chan), txflags(tfClose)); BEAST_EXPECT(!channelExists(*env.current(), chan)); BEAST_EXPECT(env.balance(alice) == preAlice + channelFunds); } // fixPayChanCancelAfter // CancelAfter should be greater than close time { for (bool const withFixPayChan : {true, false}) { auto const amend = withFixPayChan ? features : features - fixPayChanCancelAfter; Env env{*this, amend}; env.fund(XRP(10000), alice, bob); env.close(); auto const pk = alice.pk(); auto const settleDelay = 100s; auto const channelFunds = XRP(1000); NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime - 1s; auto const txResult = withFixPayChan ? ter(tecEXPIRED) : ter(tesSUCCESS); env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter), txResult); } } // fixPayChanCancelAfter // CancelAfter can be equal to the close time { for (bool const withFixPayChan : {true, false}) { auto const amend = withFixPayChan ? features : features - fixPayChanCancelAfter; Env env{*this, amend}; env.fund(XRP(10000), alice, bob); env.close(); auto const pk = alice.pk(); auto const settleDelay = 100s; auto const channelFunds = XRP(1000); NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime; env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter), ter(tesSUCCESS)); } } } void testExpiration(FeatureBitset features) { testcase("expiration"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const carol = Account("carol"); env.fund(XRP(10000), alice, bob, carol); auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const closeTime = env.current()->header().parentCloseTime; auto const minExpiration = closeTime + settleDelay; NetClock::time_point const cancelAfter = closeTime + 7200s; auto const channelFunds = XRP(1000); auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); BEAST_EXPECT(channelExists(*env.current(), chan)); BEAST_EXPECT(!channelExpiration(*env.current(), chan)); // Owner closes, will close after settleDelay env(claim(alice, chan), txflags(tfClose)); auto counts = [](auto const& t) { return t.time_since_epoch().count(); }; // NOLINTNEXTLINE(bugprone-unchecked-optional-access) BEAST_EXPECT(*channelExpiration(*env.current(), chan) == counts(minExpiration)); // increase the expiration time env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration + 100s})); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) BEAST_EXPECT(*channelExpiration(*env.current(), chan) == counts(minExpiration) + 100); // decrease the expiration, but still above minExpiration env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration + 50s})); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) BEAST_EXPECT(*channelExpiration(*env.current(), chan) == counts(minExpiration) + 50); // decrease the expiration below minExpiration env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration - 50s}), ter(temBAD_EXPIRATION)); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) BEAST_EXPECT(*channelExpiration(*env.current(), chan) == counts(minExpiration) + 50); env(claim(bob, chan), txflags(tfRenew), ter(tecNO_PERMISSION)); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) BEAST_EXPECT(*channelExpiration(*env.current(), chan) == counts(minExpiration) + 50); env(claim(alice, chan), txflags(tfRenew)); BEAST_EXPECT(!channelExpiration(*env.current(), chan)); // decrease the expiration below minExpiration env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration - 50s}), ter(temBAD_EXPIRATION)); BEAST_EXPECT(!channelExpiration(*env.current(), chan)); env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration})); env.close(minExpiration); // Try to extend the expiration after the expiration has already passed env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration + 1000s})); BEAST_EXPECT(!channelExists(*env.current(), chan)); } void testSettleDelay(FeatureBitset features) { testcase("settle delay"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 3600s; NetClock::time_point const settleTimepoint = env.current()->header().parentCloseTime + settleDelay; auto const channelFunds = XRP(1000); auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk)); BEAST_EXPECT(channelExists(*env.current(), chan)); // Owner closes, will close after settleDelay env(claim(alice, chan), txflags(tfClose)); BEAST_EXPECT(channelExists(*env.current(), chan)); env.close(settleTimepoint - settleDelay / 2); { // receiver can still claim auto const chanBal = channelBalance(*env.current(), chan); auto const chanAmt = channelAmount(*env.current(), chan); auto preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; auto const authAmt = reqBal + XRP(100); assert(reqBal <= chanAmt); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops); } env.close(settleTimepoint); { // past settleTime, channel will close auto const chanBal = channelBalance(*env.current(), chan); auto const chanAmt = channelAmount(*env.current(), chan); auto const preAlice = env.balance(alice); auto preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; auto const authAmt = reqBal + XRP(100); assert(reqBal <= chanAmt); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); BEAST_EXPECT(!channelExists(*env.current(), chan)); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(alice) == preAlice + chanAmt - chanBal); BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); } } void testCloseDry(FeatureBitset features) { testcase("close dry"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk)); BEAST_EXPECT(channelExists(*env.current(), chan)); // Owner tries to close channel, but it will remain open (settle delay) env(claim(alice, chan), txflags(tfClose)); BEAST_EXPECT(channelExists(*env.current(), chan)); { // claim the entire amount auto const preBob = env.balance(bob); env(claim(alice, chan, channelFunds.value(), channelFunds.value())); BEAST_EXPECT(channelBalance(*env.current(), chan) == channelFunds); BEAST_EXPECT(env.balance(bob) == preBob + channelFunds); } auto const preAlice = env.balance(alice); // Channel is now dry, can close before expiration date env(claim(alice, chan), txflags(tfClose)); BEAST_EXPECT(!channelExists(*env.current(), chan)); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(alice) == preAlice - feeDrops); } void testDefaultAmount(FeatureBitset features) { // auth amount defaults to balance if not present testcase("default amount"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk)); BEAST_EXPECT(channelExists(*env.current(), chan)); // Owner tries to close channel, but it will remain open (settle delay) env(claim(alice, chan), txflags(tfClose)); BEAST_EXPECT(channelExists(*env.current(), chan)); { auto chanBal = channelBalance(*env.current(), chan); auto chanAmt = channelAmount(*env.current(), chan); auto const preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; assert(reqBal <= chanAmt); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, reqBal); env(claim(bob, chan, reqBal, std::nullopt, Slice(sig), alice.pk())); BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops); chanBal = reqBal; } { // Claim again auto chanBal = channelBalance(*env.current(), chan); auto chanAmt = channelAmount(*env.current(), chan); auto const preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; assert(reqBal <= chanAmt); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, reqBal); env(claim(bob, chan, reqBal, std::nullopt, Slice(sig), alice.pk())); BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops); chanBal = reqBal; } } void testDisallowXRP(FeatureBitset features) { // auth amount defaults to balance if not present testcase("Disallow XRP"); using namespace jtx; using namespace std::literals::chrono_literals; auto const alice = Account("alice"); auto const bob = Account("bob"); { // Create a channel where dst disallows XRP. Ignore that flag, // since it's just advisory. Env env{*this, features}; env.fund(XRP(10000), alice, bob); env(fset(bob, asfDisallowXRP)); auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), 3600s, alice.pk())); BEAST_EXPECT(channelExists(*env.current(), chan)); } { // Claim to a channel where dst disallows XRP (channel is // created before disallow xrp is set). Ignore that flag // since it is just advisory. Env env{*this, features}; env.fund(XRP(10000), alice, bob); auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), 3600s, alice.pk())); BEAST_EXPECT(channelExists(*env.current(), chan)); env(fset(bob, asfDisallowXRP)); auto const reqBal = XRP(500).value(); env(claim(alice, chan, reqBal, reqBal)); } } void testDstTag(FeatureBitset features) { // auth amount defaults to balance if not present testcase("Dst Tag"); using namespace jtx; using namespace std::literals::chrono_literals; // Create a channel where dst disallows XRP Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); env.fund(XRP(10000), alice, bob); env(fset(bob, asfRequireDest)); auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); { auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk), ter(tecDST_TAG_NEEDED)); BEAST_EXPECT(!channelExists(*env.current(), chan)); } { auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk, std::nullopt, 1)); BEAST_EXPECT(channelExists(*env.current(), chan)); } } void testDepositAuth(FeatureBitset features) { testcase("Deposit Authorization"); using namespace jtx; using namespace std::literals::chrono_literals; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const carol = Account("carol"); auto USDA = alice["USD"]; { Env env{*this, features}; env.fund(XRP(10000), alice, bob, carol); env(fset(bob, asfDepositAuth)); env.close(); auto const pk = alice.pk(); auto const settleDelay = 100s; auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); // alice can add more funds to the channel even though bob has // asfDepositAuth set. env(fund(alice, chan, XRP(1000))); env.close(); // alice claims. Fails because bob's lsfDepositAuth flag is set. env(claim(alice, chan, XRP(500).value(), XRP(500).value()), ter(tecNO_PERMISSION)); env.close(); // Claim with signature auto const baseFee = env.current()->fees().base; auto const preBob = env.balance(bob); { auto const delta = XRP(500).value(); auto const sig = signClaimAuth(pk, alice.sk(), chan, delta); // alice claims with signature. Fails since bob has // lsfDepositAuth flag set. env(claim(alice, chan, delta, delta, Slice(sig), pk), ter(tecNO_PERMISSION)); env.close(); BEAST_EXPECT(env.balance(bob) == preBob); // bob claims but omits the signature. Fails because only // alice can claim without a signature. env(claim(bob, chan, delta, delta), ter(temBAD_SIGNATURE)); env.close(); // bob claims with signature. Succeeds even though bob's // lsfDepositAuth flag is set since bob submitted the // transaction. env(claim(bob, chan, delta, delta, Slice(sig), pk)); env.close(); BEAST_EXPECT(env.balance(bob) == preBob + delta - baseFee); } { // Explore the limits of deposit pre-authorization. auto const delta = XRP(600).value(); auto const sig = signClaimAuth(pk, alice.sk(), chan, delta); // carol claims and fails. Only channel participants (bob or // alice) may claim. env(claim(carol, chan, delta, delta, Slice(sig), pk), ter(tecNO_PERMISSION)); env.close(); // bob preauthorizes carol for deposit. But after that carol // still can't claim since only channel participants may claim. env(deposit::auth(bob, carol)); env.close(); env(claim(carol, chan, delta, delta, Slice(sig), pk), ter(tecNO_PERMISSION)); // Since alice is not preauthorized she also may not claim // for bob. env(claim(alice, chan, delta, delta, Slice(sig), pk), ter(tecNO_PERMISSION)); env.close(); // However if bob preauthorizes alice for deposit then she can // successfully submit a claim. env(deposit::auth(bob, alice)); env.close(); env(claim(alice, chan, delta, delta, Slice(sig), pk)); env.close(); BEAST_EXPECT(env.balance(bob) == preBob + delta - (3 * baseFee)); } { // bob removes pre-authorization of alice. Once again she // cannot submit a claim. auto const delta = XRP(800).value(); env(deposit::unauth(bob, alice)); env.close(); // alice claims and fails since she is no longer preauthorized. env(claim(alice, chan, delta, delta), ter(tecNO_PERMISSION)); env.close(); // bob clears lsfDepositAuth. Now alice can claim. env(fclear(bob, asfDepositAuth)); env.close(); // alice claims successfully. env(claim(alice, chan, delta, delta)); env.close(); BEAST_EXPECT(env.balance(bob) == preBob + XRP(800) - (5 * baseFee)); } } } void testDepositAuthCreds() { testcase("Deposit Authorization with Credentials"); using namespace jtx; using namespace std::literals::chrono_literals; char const credType[] = "abcde"; Account const alice("alice"); Account const bob("bob"); Account const carol("carol"); Account const dillon("dillon"); Account const zelda("zelda"); { Env env{*this}; env.fund(XRP(10000), alice, bob, carol, dillon, zelda); auto const pk = alice.pk(); auto const settleDelay = 100s; auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); // alice add funds to the channel env(fund(alice, chan, XRP(1000))); env.close(); std::string const credBadIdx = "D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6" "E1"; auto const delta = XRP(500).value(); { // create credentials auto jv = credentials::create(alice, carol, credType); uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count() + 100; jv[sfExpiration.jsonName] = t; env(jv); env.close(); } auto const jv = credentials::ledgerEntry(env, alice, carol, credType); std::string const credIdx = jv[jss::result][jss::index].asString(); // Bob require pre-authorization env(fset(bob, asfDepositAuth)); env.close(); // Fail, credentials not accepted env(claim(alice, chan, delta, delta), credentials::ids({credIdx}), ter(tecBAD_CREDENTIALS)); env.close(); env(credentials::accept(alice, carol, credType)); env.close(); // Fail, no depositPreauth object env(claim(alice, chan, delta, delta), credentials::ids({credIdx}), ter(tecNO_PERMISSION)); env.close(); // Setup deposit authorization env(deposit::authCredentials(bob, {{carol, credType}})); env.close(); // Fail, credentials doesn’t belong to root account env(claim(dillon, chan, delta, delta), credentials::ids({credIdx}), ter(tecBAD_CREDENTIALS)); // Fails because bob's lsfDepositAuth flag is set. env(claim(alice, chan, delta, delta), ter(tecNO_PERMISSION)); // Fail, bad credentials index. env(claim(alice, chan, delta, delta), credentials::ids({credBadIdx}), ter(tecBAD_CREDENTIALS)); // Fail, empty credentials env(claim(alice, chan, delta, delta), credentials::ids({}), ter(temMALFORMED)); { // claim fails cause of expired credentials // Every cycle +10sec. for (int i = 0; i < 10; ++i) env.close(); env(claim(alice, chan, delta, delta), credentials::ids({credIdx}), ter(tecEXPIRED)); env.close(); } { // create credentials once more env(credentials::create(alice, carol, credType)); env.close(); env(credentials::accept(alice, carol, credType)); env.close(); auto const jv = credentials::ledgerEntry(env, alice, carol, credType); std::string const credIdx = jv[jss::result][jss::index].asString(); // Success env(claim(alice, chan, delta, delta), credentials::ids({credIdx})); } } { Env env{*this}; env.fund(XRP(10000), alice, bob, carol, dillon, zelda); auto const pk = alice.pk(); auto const settleDelay = 100s; auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); // alice add funds to the channel env(fund(alice, chan, XRP(1000))); env.close(); auto const delta = XRP(500).value(); { // create credentials env(credentials::create(alice, carol, credType)); env.close(); env(credentials::accept(alice, carol, credType)); env.close(); } auto const jv = credentials::ledgerEntry(env, alice, carol, credType); std::string const credIdx = jv[jss::result][jss::index].asString(); // Succeed, lsfDepositAuth is not set env(claim(alice, chan, delta, delta), credentials::ids({credIdx})); env.close(); } { // Credentials amendment not enabled Env env(*this, testable_amendments() - featureCredentials); env.fund(XRP(5000), "alice", "bob"); env.close(); auto const pk = alice.pk(); auto const settleDelay = 100s; auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); env(fund(alice, chan, XRP(1000))); env.close(); std::string const credIdx = "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" "E4"; // can't claim with old DepositPreauth because rule is not enabled. env(fset(bob, asfDepositAuth)); env.close(); env(deposit::auth(bob, alice)); env.close(); env(claim(alice, chan, XRP(500).value(), XRP(500).value()), credentials::ids({credIdx}), ter(temDISABLED)); } } void testMultiple(FeatureBitset features) { // auth amount defaults to balance if not present testcase("Multiple channels to the same account"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); auto const chan1 = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk)); BEAST_EXPECT(channelExists(*env.current(), chan1)); auto const chan2 = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk)); BEAST_EXPECT(channelExists(*env.current(), chan2)); BEAST_EXPECT(chan1 != chan2); } void testAccountChannelsRPC(FeatureBitset features) { testcase("AccountChannels RPC"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const charlie = Account("charlie", KeyType::ed25519); env.fund(XRP(10000), alice, bob, charlie); auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); auto const chan1Str = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); env.close(); { // test account non-string auto testInvalidAccountParam = [&](auto const& param) { Json::Value params; params[jss::account] = param; auto jrr = env.rpc("json", "account_channels", to_string(params))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "invalidParams"); BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'."); }; testInvalidAccountParam(1); testInvalidAccountParam(1.1); testInvalidAccountParam(true); testInvalidAccountParam(Json::Value(Json::nullValue)); testInvalidAccountParam(Json::Value(Json::objectValue)); testInvalidAccountParam(Json::Value(Json::arrayValue)); } { auto const r = env.rpc("account_channels", alice.human(), bob.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str); BEAST_EXPECT(r[jss::result][jss::validated]); } { auto const r = env.rpc("account_channels", alice.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str); BEAST_EXPECT(r[jss::result][jss::validated]); } { auto const r = env.rpc("account_channels", bob.human(), alice.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 0); BEAST_EXPECT(r[jss::result][jss::validated]); } auto const chan2Str = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); env.close(); { auto const r = env.rpc("account_channels", alice.human(), bob.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 2); BEAST_EXPECT(r[jss::result][jss::validated]); BEAST_EXPECT(chan1Str != chan2Str); for (auto const& c : {chan1Str, chan2Str}) BEAST_EXPECT( r[jss::result][jss::channels][0u][jss::channel_id] == c || r[jss::result][jss::channels][1u][jss::channel_id] == c); } } void testAccountChannelsRPCMarkers(FeatureBitset features) { testcase("Account channels RPC markers"); using namespace test::jtx; using namespace std::literals; auto const alice = Account("alice"); auto const bobs = []() -> std::vector { int const n = 10; std::vector r; r.reserve(n); for (int i = 0; i < n; ++i) { r.emplace_back("bob"s + std::to_string(i)); } return r; }(); Env env{*this, features}; env.fund(XRP(10000), alice); for (auto const& a : bobs) { env.fund(XRP(10000), a); env.close(); } { // create a channel from alice to every bob account auto const settleDelay = 3600s; auto const channelFunds = XRP(1); for (auto const& b : bobs) { env(create(alice, b, channelFunds, settleDelay, alice.pk())); } } auto testLimit = [](test::jtx::Env& env, test::jtx::Account const& src, std::optional limit = std::nullopt, Json::Value const& marker = Json::nullValue, std::optional const& dst = std::nullopt) { Json::Value jvc; jvc[jss::account] = src.human(); if (dst) jvc[jss::destination_account] = dst->human(); if (limit) jvc[jss::limit] = *limit; if (marker) jvc[jss::marker] = marker; return env.rpc("json", "account_channels", to_string(jvc))[jss::result]; }; { // No marker auto const r = testLimit(env, alice); BEAST_EXPECT(r.isMember(jss::channels)); BEAST_EXPECT(r[jss::channels].size() == bobs.size()); } auto const bobsB58 = [&bobs]() -> std::set { std::set r; for (auto const& a : bobs) r.insert(a.human()); return r; }(); for (int limit = 1; limit < bobs.size() + 1; ++limit) { auto leftToFind = bobsB58; auto const numFull = bobs.size() / limit; auto const numNonFull = bobs.size() % limit ? 1 : 0; Json::Value marker = Json::nullValue; auto const testIt = [&](bool expectMarker, int expectedBatchSize) { auto const r = testLimit(env, alice, limit, marker); BEAST_EXPECT(!expectMarker || r.isMember(jss::marker)); if (r.isMember(jss::marker)) marker = r[jss::marker]; BEAST_EXPECT(r[jss::channels].size() == expectedBatchSize); auto const c = r[jss::channels]; auto const s = r[jss::channels].size(); for (int j = 0; j < s; ++j) { auto const dstAcc = c[j][jss::destination_account].asString(); BEAST_EXPECT(leftToFind.count(dstAcc)); leftToFind.erase(dstAcc); } }; for (int i = 0; i < numFull; ++i) { bool const expectMarker = (numNonFull != 0 || i < numFull - 1); testIt(expectMarker, limit); } if (numNonFull) { testIt(false, bobs.size() % limit); } BEAST_EXPECT(leftToFind.empty()); } { // degenerate case auto const r = testLimit(env, alice, 0); BEAST_EXPECT(r.isMember(jss::error_message)); } } void testAccountChannelsRPCSenderOnly(FeatureBitset features) { // Check that the account_channels command only returns channels owned // by the account testcase("Account channels RPC owner only"); using namespace test::jtx; using namespace std::literals; auto const alice = Account("alice"); auto const bob = Account("bob"); Env env{*this, features}; env.fund(XRP(10000), alice, bob); // Create a channel from alice to bob and from bob to alice // When retrieving alice's channels, it should only retrieve the // channels where alice is the source, not the destination auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); env(create(alice, bob, channelFunds, settleDelay, alice.pk())); env(create(bob, alice, channelFunds, settleDelay, bob.pk())); auto const r = [&] { Json::Value jvc; jvc[jss::account] = alice.human(); return env.rpc("json", "account_channels", to_string(jvc))[jss::result]; }(); BEAST_EXPECT(r.isMember(jss::channels)); BEAST_EXPECT(r[jss::channels].size() == 1); BEAST_EXPECT(r[jss::channels][0u][jss::destination_account].asString() == bob.human()); } void testAccountChannelAuthorize(FeatureBitset features) { using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const charlie = Account("charlie", KeyType::ed25519); env.fund(XRP(10000), alice, bob, charlie); auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); auto const chan1Str = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); env.close(); Json::Value args{Json::objectValue}; args[jss::channel_id] = chan1Str; args[jss::key_type] = "ed255191"; args[jss::seed] = "snHq1rzQoN2qiUkC3XF5RyxBzUtN"; args[jss::amount] = 51110000; // test for all api versions forAllApiVersions([&, this](unsigned apiVersion) { testcase("PayChan Channel_Auth RPC Api " + std::to_string(apiVersion)); args[jss::api_version] = apiVersion; auto const rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result]; auto const error = apiVersion < 2u ? "invalidParams" : "badKeyType"; BEAST_EXPECT(rs[jss::error] == error); }); } void testAuthVerifyRPC(FeatureBitset features) { testcase("PayChan Auth/Verify RPC"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const charlie = Account("charlie", KeyType::ed25519); env.fund(XRP(10000), alice, bob, charlie); auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); auto const chan1Str = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); env.close(); std::string chan1PkStr; { auto const r = env.rpc("account_channels", alice.human(), bob.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str); BEAST_EXPECT(r[jss::result][jss::validated]); chan1PkStr = r[jss::result][jss::channels][0u][jss::public_key].asString(); } { auto const r = env.rpc("account_channels", alice.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str); BEAST_EXPECT(r[jss::result][jss::validated]); chan1PkStr = r[jss::result][jss::channels][0u][jss::public_key].asString(); } { auto const r = env.rpc("account_channels", bob.human(), alice.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 0); BEAST_EXPECT(r[jss::result][jss::validated]); } auto const chan2Str = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); env.close(); { auto const r = env.rpc("account_channels", alice.human(), bob.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 2); BEAST_EXPECT(r[jss::result][jss::validated]); BEAST_EXPECT(chan1Str != chan2Str); for (auto const& c : {chan1Str, chan2Str}) BEAST_EXPECT( r[jss::result][jss::channels][0u][jss::channel_id] == c || r[jss::result][jss::channels][1u][jss::channel_id] == c); } auto sliceToHex = [](Slice const& slice) { std::string s; s.reserve(2 * slice.size()); for (int i = 0; i < slice.size(); ++i) { s += "0123456789ABCDEF"[((slice[i] & 0xf0) >> 4)]; s += "0123456789ABCDEF"[((slice[i] & 0x0f) >> 0)]; } return s; }; { // Verify chan1 auth auto const rs = env.rpc("channel_authorize", "alice", chan1Str, "1000"); auto const sig = rs[jss::result][jss::signature].asString(); BEAST_EXPECT(!sig.empty()); { auto const rv = env.rpc("channel_verify", chan1PkStr, chan1Str, "1000", sig); BEAST_EXPECT(rv[jss::result][jss::signature_verified].asBool()); } { // use pk hex to verify auto const pkAsHex = sliceToHex(pk.slice()); auto const rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000", sig); BEAST_EXPECT(rv[jss::result][jss::signature_verified].asBool()); } { // malformed amount auto const pkAsHex = sliceToHex(pk.slice()); auto rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000x", sig); BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000 ", sig); BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); rv = env.rpc("channel_verify", pkAsHex, chan1Str, "x1000", sig); BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); rv = env.rpc("channel_verify", pkAsHex, chan1Str, "x", sig); BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); rv = env.rpc("channel_verify", pkAsHex, chan1Str, " ", sig); BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000 1000", sig); BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1,000", sig); BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); rv = env.rpc("channel_verify", pkAsHex, chan1Str, " 1000", sig); BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); rv = env.rpc("channel_verify", pkAsHex, chan1Str, "", sig); BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); } { // malformed channel auto const pkAsHex = sliceToHex(pk.slice()); auto chan1StrBad = chan1Str; chan1StrBad.pop_back(); auto rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig); BEAST_EXPECT(rv[jss::error] == "channelMalformed"); rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000"); BEAST_EXPECT(rv[jss::error] == "channelMalformed"); chan1StrBad = chan1Str; chan1StrBad.push_back('0'); rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig); BEAST_EXPECT(rv[jss::error] == "channelMalformed"); rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000"); BEAST_EXPECT(rv[jss::error] == "channelMalformed"); chan1StrBad = chan1Str; chan1StrBad.back() = 'x'; rv = env.rpc("channel_verify", pkAsHex, chan1StrBad, "1000", sig); BEAST_EXPECT(rv[jss::error] == "channelMalformed"); rv = env.rpc("channel_authorize", "alice", chan1StrBad, "1000"); BEAST_EXPECT(rv[jss::error] == "channelMalformed"); } { // give an ill formed base 58 public key auto illFormedPk = chan1PkStr.substr(0, chan1PkStr.size() - 1); auto const rv = env.rpc("channel_verify", illFormedPk, chan1Str, "1000", sig); BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool()); } { // give an ill formed hex public key auto const pkAsHex = sliceToHex(pk.slice()); auto illFormedPk = pkAsHex.substr(0, chan1PkStr.size() - 1); auto const rv = env.rpc("channel_verify", illFormedPk, chan1Str, "1000", sig); BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool()); } } { // Try to verify chan2 auth with chan1 key auto const rs = env.rpc("channel_authorize", "alice", chan2Str, "1000"); auto const sig = rs[jss::result][jss::signature].asString(); BEAST_EXPECT(!sig.empty()); { auto const rv = env.rpc("channel_verify", chan1PkStr, chan1Str, "1000", sig); BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool()); } { // use pk hex to verify auto const pkAsHex = sliceToHex(pk.slice()); auto const rv = env.rpc("channel_verify", pkAsHex, chan1Str, "1000", sig); BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool()); } } { // Try to explicitly specify secp256k1 and Ed25519 keys: auto const chan = to_string(channel(charlie, alice, env.seq(charlie))); env(create(charlie, alice, channelFunds, settleDelay, charlie.pk())); env.close(); std::string cpk; { auto const r = env.rpc("account_channels", charlie.human(), alice.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan); BEAST_EXPECT(r[jss::result][jss::validated]); cpk = r[jss::result][jss::channels][0u][jss::public_key].asString(); } // Try to authorize without specifying a key type, expect an error: auto const rs = env.rpc("channel_authorize", "charlie", chan, "1000"); auto const sig = rs[jss::result][jss::signature].asString(); BEAST_EXPECT(!sig.empty()); { auto const rv = env.rpc("channel_verify", cpk, chan, "1000", sig); BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool()); } // Try to authorize using an unknown key type, except an error: auto const rs1 = env.rpc("channel_authorize", "charlie", "nyx", chan, "1000"); BEAST_EXPECT(rs1[jss::error] == "badKeyType"); // Try to authorize using secp256k1; the authorization _should_ // succeed but the verification should fail: auto const rs2 = env.rpc("channel_authorize", "charlie", "secp256k1", chan, "1000"); auto const sig2 = rs2[jss::result][jss::signature].asString(); BEAST_EXPECT(!sig2.empty()); { auto const rv = env.rpc("channel_verify", cpk, chan, "1000", sig2); BEAST_EXPECT(!rv[jss::result][jss::signature_verified].asBool()); } // Try to authorize using Ed25519; expect success: auto const rs3 = env.rpc("channel_authorize", "charlie", "ed25519", chan, "1000"); auto const sig3 = rs3[jss::result][jss::signature].asString(); BEAST_EXPECT(!sig3.empty()); { auto const rv = env.rpc("channel_verify", cpk, chan, "1000", sig3); BEAST_EXPECT(rv[jss::result][jss::signature_verified].asBool()); } } { // send malformed amounts rpc requests auto rs = env.rpc("channel_authorize", "alice", chan1Str, "1000x"); BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed"); rs = env.rpc("channel_authorize", "alice", chan1Str, "x1000"); BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed"); rs = env.rpc("channel_authorize", "alice", chan1Str, "x"); BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed"); { // Missing channel_id Json::Value args{Json::objectValue}; args[jss::amount] = "2000"; args[jss::key_type] = "secp256k1"; args[jss::passphrase] = "passphrase_can_be_anything"; rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result]; BEAST_EXPECT(rs[jss::error] == "invalidParams"); } { // Missing amount Json::Value args{Json::objectValue}; args[jss::channel_id] = chan1Str; args[jss::key_type] = "secp256k1"; args[jss::passphrase] = "passphrase_can_be_anything"; rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result]; BEAST_EXPECT(rs[jss::error] == "invalidParams"); } { // Missing key_type and no secret. Json::Value args{Json::objectValue}; args[jss::amount] = "2000"; args[jss::channel_id] = chan1Str; args[jss::passphrase] = "passphrase_can_be_anything"; rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result]; BEAST_EXPECT(rs[jss::error] == "invalidParams"); } { // Both passphrase and seed specified. Json::Value args{Json::objectValue}; args[jss::amount] = "2000"; args[jss::channel_id] = chan1Str; args[jss::key_type] = "secp256k1"; args[jss::passphrase] = "passphrase_can_be_anything"; args[jss::seed] = "seed can be anything"; rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result]; BEAST_EXPECT(rs[jss::error] == "invalidParams"); } { // channel_id is not exact hex. Json::Value args{Json::objectValue}; args[jss::amount] = "2000"; args[jss::channel_id] = chan1Str + "1"; args[jss::key_type] = "secp256k1"; args[jss::passphrase] = "passphrase_can_be_anything"; rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result]; BEAST_EXPECT(rs[jss::error] == "channelMalformed"); } { // amount is not a string Json::Value args{Json::objectValue}; args[jss::amount] = 2000; args[jss::channel_id] = chan1Str; args[jss::key_type] = "secp256k1"; args[jss::passphrase] = "passphrase_can_be_anything"; rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result]; BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed"); } { // Amount is not a decimal string. Json::Value args{Json::objectValue}; args[jss::amount] = "TwoThousand"; args[jss::channel_id] = chan1Str; args[jss::key_type] = "secp256k1"; args[jss::passphrase] = "passphrase_can_be_anything"; rs = env.rpc("json", "channel_authorize", args.toStyledString())[jss::result]; BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed"); } } } void testOptionalFields(FeatureBitset features) { testcase("Optional Fields"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const carol = Account("carol"); auto const dan = Account("dan"); env.fund(XRP(10000), alice, bob, carol, dan); auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); std::optional cancelAfter; { auto const chan = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); auto const r = env.rpc("account_channels", alice.human(), bob.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan); BEAST_EXPECT(!r[jss::result][jss::channels][0u].isMember(jss::destination_tag)); } { std::uint32_t dstTag = 42; auto const chan = to_string(channel(alice, carol, env.seq(alice))); env(create(alice, carol, channelFunds, settleDelay, pk, cancelAfter, dstTag)); auto const r = env.rpc("account_channels", alice.human(), carol.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::channel_id] == chan); BEAST_EXPECT(r[jss::result][jss::channels][0u][jss::destination_tag] == dstTag); } } void testMalformedPK(FeatureBitset features) { testcase("malformed pk"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); auto USDA = alice["USD"]; env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 100s; auto const chan = channel(alice, bob, env.seq(alice)); auto jv = create(alice, bob, XRP(1000), settleDelay, pk); auto const pkHex = strHex(pk.slice()); jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2); env(jv, ter(temMALFORMED)); jv["PublicKey"] = pkHex.substr(0, pkHex.size() - 2); env(jv, ter(temMALFORMED)); auto badPrefix = pkHex; badPrefix[0] = 'f'; badPrefix[1] = 'f'; jv["PublicKey"] = badPrefix; env(jv, ter(temMALFORMED)); jv["PublicKey"] = pkHex; env(jv); auto const authAmt = XRP(100); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); jv = claim(bob, chan, authAmt.value(), authAmt.value(), Slice(sig), alice.pk()); jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2); env(jv, ter(temMALFORMED)); jv["PublicKey"] = pkHex.substr(0, pkHex.size() - 2); env(jv, ter(temMALFORMED)); badPrefix = pkHex; badPrefix[0] = 'f'; badPrefix[1] = 'f'; jv["PublicKey"] = badPrefix; env(jv, ter(temMALFORMED)); // missing public key jv.removeMember("PublicKey"); env(jv, ter(temMALFORMED)); { auto const txn = R"*( { "channel_id":"5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3", "signature": "304402204EF0AFB78AC23ED1C472E74F4299C0C21F1B21D07EFC0A3838A420F76D783A400220154FB11B6F54320666E4C36CA7F686C16A3A0456800BBC43746F34AF50290064", "public_key": "aKijDDiC2q2gXjMpM7i4BUS6cmixgsEe18e7CjsUxwihKfuoFgS5", "amount": "1000000" } )*"; auto const r = env.rpc("json", "channel_verify", txn); BEAST_EXPECT(r["result"]["error"] == "publicMalformed"); } } void testMetaAndOwnership(FeatureBitset features) { testcase("Metadata & Ownership"); using namespace jtx; using namespace std::literals::chrono_literals; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const settleDelay = 100s; auto const pk = alice.pk(); auto inOwnerDir = [](ReadView const& view, Account const& acc, std::shared_ptr const& chan) -> bool { xrpl::Dir const ownerDir(view, keylet::ownerDir(acc.id())); return std::find(ownerDir.begin(), ownerDir.end(), chan) != ownerDir.end(); }; auto ownerDirCount = [](ReadView const& view, Account const& acc) -> std::size_t { xrpl::Dir const ownerDir(view, keylet::ownerDir(acc.id())); return std::distance(ownerDir.begin(), ownerDir.end()); }; { // Test with adding the paychan to the recipient's owner directory Env env{*this, features}; env.fund(XRP(10000), alice, bob); env(create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); auto const [chan, chanSle] = channelKeyAndSle(*env.current(), alice, bob); BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); BEAST_EXPECT(inOwnerDir(*env.current(), bob, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); // close the channel env(claim(bob, chan), txflags(tfClose)); BEAST_EXPECT(!channelExists(*env.current(), chan)); BEAST_EXPECT(!inOwnerDir(*env.current(), alice, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0); BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0); } { // Test removing paychans created before adding to the recipient's // owner directory Env env(*this, features); env.fund(XRP(10000), alice, bob); // create the channel before the amendment activates env(create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); auto const [chan, chanSle] = channelKeyAndSle(*env.current(), alice, bob); BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); BEAST_EXPECT(inOwnerDir(*env.current(), bob, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); env(claim(bob, chan), txflags(tfClose)); BEAST_EXPECT(!channelExists(*env.current(), chan)); BEAST_EXPECT(!inOwnerDir(*env.current(), alice, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0); BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle)); BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0); } } void testAccountDelete(FeatureBitset features) { testcase("Account Delete"); using namespace test::jtx; using namespace std::literals::chrono_literals; auto rmAccount = [this]( Env& env, Account const& toRm, Account const& dst, TER expectedTer = tesSUCCESS) { // only allow an account to be deleted if the account's sequence // number is at least 256 less than the current ledger sequence for (auto minRmSeq = env.seq(toRm) + 257; env.current()->seq() < minRmSeq; env.close()) { } env(acctdelete(toRm, dst), fee(drops(env.current()->fees().increment)), ter(expectedTer)); env.close(); this->BEAST_EXPECT( isTesSuccess(expectedTer) == !env.closed()->exists(keylet::account(toRm.id()))); }; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const carol = Account("carol"); { Env env{*this, features}; env.fund(XRP(10000), alice, bob, carol); env.close(); // Create a channel from alice to bob auto const pk = alice.pk(); auto const settleDelay = 100s; auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); rmAccount(env, alice, carol, tecHAS_OBLIGATIONS); rmAccount(env, bob, carol, TER(tecHAS_OBLIGATIONS)); auto const feeDrops = env.current()->fees().base; auto chanBal = channelBalance(*env.current(), chan); auto chanAmt = channelAmount(*env.current(), chan); BEAST_EXPECT(chanBal == XRP(0)); BEAST_EXPECT(chanAmt == XRP(1000)); auto preBob = env.balance(bob); auto const delta = XRP(50); auto reqBal = chanBal + delta; auto authAmt = reqBal + XRP(100); assert(reqBal <= chanAmt); env(claim(alice, chan, reqBal, authAmt)); env.close(); BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob + delta); chanBal = reqBal; auto const preAlice = env.balance(alice); env(fund(alice, chan, XRP(1000))); env.close(); BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt + XRP(1000)); chanAmt = chanAmt + XRP(1000); { // Owner closes, will close after settleDelay env(claim(alice, chan), txflags(tfClose)); env.close(); // settle delay hasn't elapsed. Channels should exist. BEAST_EXPECT(channelExists(*env.current(), chan)); auto const closeTime = env.current()->header().parentCloseTime; auto const minExpiration = closeTime + settleDelay; env.close(minExpiration); env(claim(alice, chan), txflags(tfClose)); BEAST_EXPECT(!channelExists(*env.current(), chan)); } } } void testUsingTickets(FeatureBitset features) { testcase("using tickets"); using namespace jtx; using namespace std::literals::chrono_literals; Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); auto USDA = alice["USD"]; env.fund(XRP(10000), alice, bob); // alice and bob grab enough tickets for all of the following // transactions. Note that once the tickets are acquired alice's // and bob's account sequence numbers should not advance. std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; env(ticket::create(alice, 10)); std::uint32_t const aliceSeq{env.seq(alice)}; std::uint32_t bobTicketSeq{env.seq(bob) + 1}; env(ticket::create(bob, 10)); std::uint32_t const bobSeq{env.seq(bob)}; auto const pk = alice.pk(); auto const settleDelay = 100s; auto const chan = channel(alice, bob, aliceTicketSeq); env(create(alice, bob, XRP(1000), settleDelay, pk), ticket::use(aliceTicketSeq++)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); BEAST_EXPECT(env.seq(alice) == aliceSeq); BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); { auto const preAlice = env.balance(alice); env(fund(alice, chan, XRP(1000)), ticket::use(aliceTicketSeq++)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); BEAST_EXPECT(env.seq(alice) == aliceSeq); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops); } auto chanBal = channelBalance(*env.current(), chan); auto chanAmt = channelAmount(*env.current(), chan); BEAST_EXPECT(chanBal == XRP(0)); BEAST_EXPECT(chanAmt == XRP(2000)); { // No signature needed since the owner is claiming auto const preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; auto const authAmt = reqBal + XRP(100); assert(reqBal <= chanAmt); env(claim(alice, chan, reqBal, authAmt), ticket::use(aliceTicketSeq++)); env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); BEAST_EXPECT(env.seq(alice) == aliceSeq); BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob + delta); chanBal = reqBal; } { // Claim with signature auto preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; auto const authAmt = reqBal + XRP(100); assert(reqBal <= chanAmt); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), ticket::use(bobTicketSeq++)); env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); auto const feeDrops = env.current()->fees().base; BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops); chanBal = reqBal; // claim again preBob = env.balance(bob); // A transaction that generates a tec still consumes its ticket. env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), ticket::use(bobTicketSeq++), ter(tecUNFUNDED_PAYMENT)); env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); } { // Try to claim more than authorized auto const preBob = env.balance(bob); STAmount const authAmt = chanBal + XRP(500); STAmount const reqAmt = authAmt + drops(1); assert(reqAmt <= chanAmt); // Note that since claim() returns a tem (neither tec nor tes), // the ticket is not consumed. So we don't increment bobTicket. auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); env(claim(bob, chan, reqAmt, authAmt, Slice(sig), alice.pk()), ticket::use(bobTicketSeq), ter(temBAD_AMOUNT)); env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); BEAST_EXPECT(env.balance(bob) == preBob); } // Dst tries to fund the channel env(fund(bob, chan, XRP(1000)), ticket::use(bobTicketSeq++), ter(tecNO_PERMISSION)); env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); { // Dst closes channel auto const preAlice = env.balance(alice); auto const preBob = env.balance(bob); env(claim(bob, chan), txflags(tfClose), ticket::use(bobTicketSeq++)); env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); BEAST_EXPECT(!channelExists(*env.current(), chan)); auto const feeDrops = env.current()->fees().base; auto const delta = chanAmt - chanBal; assert(delta > beast::zero); BEAST_EXPECT(env.balance(alice) == preAlice + delta); BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); } env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); BEAST_EXPECT(env.seq(alice) == aliceSeq); env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); BEAST_EXPECT(env.seq(bob) == bobSeq); } void testWithFeats(FeatureBitset features) { testSimple(features); testDisallowIncoming(features); testCancelAfter(features); testSettleDelay(features); testExpiration(features); testCloseDry(features); testDefaultAmount(features); testDisallowXRP(features); testDstTag(features); testDepositAuth(features); testMultiple(features); testAccountChannelsRPC(features); testAccountChannelsRPCMarkers(features); testAccountChannelsRPCSenderOnly(features); testAccountChannelAuthorize(features); testAuthVerifyRPC(features); testOptionalFields(features); testMalformedPK(features); testMetaAndOwnership(features); testAccountDelete(features); testUsingTickets(features); } public: void run() override { using namespace test::jtx; FeatureBitset const all{testable_amendments()}; testWithFeats(all); testDepositAuthCreds(); testMetaAndOwnership(all - fixIncludeKeyletFields); } }; BEAST_DEFINE_TESTSUITE(PayChan, app, xrpl); } // namespace test } // namespace xrpl