diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index 5068472135..703bcf51a7 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -13,13 +13,18 @@ #include #include +#include +#include +#include + #include #include #include #include #include #include -#include // IWYU pragma: keep +#include +#include #include #include #include @@ -39,6 +44,9 @@ #include #include #include +#include +#include +#include #include #include @@ -48,6 +56,7 @@ #include #include #include +#include #include #include @@ -495,7 +504,7 @@ struct PayChan_test : public beast::unit_test::Suite // Owner closes, will close after settleDelay env(claim(alice, chan), Txflags(tfClose)); BEAST_EXPECT(channelExists(*env.current(), chan)); - env.close(settleTimepoint - settleDelay / 2); + env.close(settleTimepoint - (settleDelay / 2)); { // receiver can still claim auto const chanBal = channelBalance(*env.current(), chan); @@ -1587,6 +1596,146 @@ struct PayChan_test : public beast::unit_test::Suite } } + void + testChannelVerifyLoadType(FeatureBitset features) + { + testcase("channel_verify sets kFEE_HEAVY_BURDEN_RPC load type"); + + 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 chanStr = to_string(channel(alice, bob, env.seq(alice))); + + env(create(alice, bob, channelFunds, settleDelay, pk)); + env.close(); + + // Step 1: get a valid signature from channel_authorize + auto const authResult = env.rpc("channel_authorize", "alice", chanStr, "1000"); + auto const sig = authResult[jss::result][jss::signature].asString(); + BEAST_EXPECT(!sig.empty()); + auto const pkHex = strHex(pk.slice()); + + // Step 2: build RPC::JsonContext directly so we can inspect loadType + auto& app = env.app(); + Resource::Charge loadType = Resource::kFeeReferenceRpc; + Resource::Consumer c; + RPC::JsonContext context{ + {.j = env.journal, + .app = app, + .loadType = loadType, + .netOps = app.getOPs(), + .ledgerMaster = app.getLedgerMaster(), + .consumer = c, + .role = Role::USER, + .coro = {}, + .infoSub = {}, + .apiVersion = RPC::kApiVersionIfUnspecified}, + {}, + {}}; + json::Value params; + params[jss::public_key] = pkHex; + params[jss::channel_id] = chanStr; + params[jss::amount] = "1000"; + params[jss::signature] = sig; + context.params = std::move(params); + + // Confirm default before calling handler + BEAST_EXPECT(context.loadType == Resource::kFeeReferenceRpc); + json::Value result; + Gate g; + app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { + context.coro = coro; + result = doChannelVerify(context); + g.signal(); + }); + + using namespace std::chrono_literals; + BEAST_EXPECT(g.waitFor(5s)); + // Signature must verify correctly + BEAST_EXPECT(result[jss::signature_verified].asBool()); + // KEY ASSERTION: loadType must be kFEE_HEAVY_BURDEN_RPC after the fix + // Before fix: this will FAIL because loadType stays kFEE_REFERENCE_RPC (20) + // After fix: this will PASS because loadType is kFEE_HEAVY_BURDEN_RPC (3000) + BEAST_EXPECT(context.loadType == Resource::kFeeHeavyBurdenRpc); + // Confirm the charge is 150x heavier than the current (broken) default + BEAST_EXPECT(context.loadType.cost() == Resource::kFeeHeavyBurdenRpc.cost()); // 3000 + BEAST_EXPECT(context.loadType.cost() != Resource::kFeeReferenceRpc.cost()); // not 20 + } + + void + testChannelAuthorizeLoadType(FeatureBitset features) + { + testcase("channel_authorize sets kFEE_HEAVY_BURDEN_RPC load type"); + + 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 chanStr = to_string(channel(alice, bob, env.seq(alice))); + + env(create(alice, bob, XRP(1000), settleDelay, pk)); + env.close(); + + auto& app = env.app(); + Resource::Charge loadType = Resource::kFeeReferenceRpc; + Resource::Consumer c; + RPC::JsonContext context{ + {.j = env.journal, + .app = app, + .loadType = loadType, + .netOps = app.getOPs(), + .ledgerMaster = app.getLedgerMaster(), + .consumer = c, + .role = Role::ADMIN, // channel_authorize requires ADMIN or canSign() + .coro = {}, + .infoSub = {}, + .apiVersion = RPC::kApiVersionIfUnspecified}, + {}, + {}}; + json::Value params; + params[jss::channel_id] = chanStr; + params[jss::amount] = "1000"; + params[jss::secret] = alice.name(); // use account name as seed + context.params = std::move(params); + + // Confirm default before calling handler + BEAST_EXPECT(context.loadType == Resource::kFeeReferenceRpc); + json::Value result; + Gate g; + app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { + context.coro = coro; + result = doChannelAuthorize(context); + g.signal(); + }); + + using namespace std::chrono_literals; + + BEAST_EXPECT(g.waitFor(5s)); + // Must return a valid signature + BEAST_EXPECT(result.isMember(jss::signature)); + BEAST_EXPECT(!result[jss::signature].asString().empty()); + // KEY ASSERTION: loadType must be kFEE_HEAVY_BURDEN_RPC after the fix + // Before fix: FAILS — stays at kFEE_REFERENCE_RPC (charge=20) + // After fix: PASSES — set to kFEE_HEAVY_BURDEN_RPC (charge=3000) + BEAST_EXPECT(context.loadType == Resource::kFeeHeavyBurdenRpc); + } + void testMalformedPK(FeatureBitset features) { @@ -1983,6 +2132,8 @@ struct PayChan_test : public beast::unit_test::Suite testMetaAndOwnership(features); testAccountDelete(features); testUsingTickets(features); + testChannelVerifyLoadType(features); + testChannelAuthorizeLoadType(features); } public: diff --git a/src/xrpld/rpc/handlers/ChannelVerify.cpp b/src/xrpld/rpc/handlers/ChannelVerify.cpp index 64f616e829..b018b70516 100644 --- a/src/xrpld/rpc/handlers/ChannelVerify.cpp +++ b/src/xrpld/rpc/handlers/ChannelVerify.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,8 @@ doChannelVerify(RPC::JsonContext& context) return RPC::missingFieldError(p); } + context.loadType = Resource::kFeeHeavyBurdenRpc; + std::optional pk; { std::string const strPk = params[jss::public_key].asString(); diff --git a/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp index be3ce13d45..d304992261 100644 --- a/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp +++ b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,8 @@ doChannelAuthorize(RPC::JsonContext& context) return RPC::makeError(RpcNotSupported, "Signing is not supported by this server."); } + context.loadType = Resource::kFeeHeavyBurdenRpc; + auto const& params(context.params); for (auto const& p : {jss::channel_id, jss::amount}) {