From 9d8b7d7213e360ed07d1105a4f48e701dbfc87d9 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 31 Jul 2025 00:49:07 -0400 Subject: [PATCH 01/32] the naive solution seems to work --- src/test/rpc/Simulate_test.cpp | 112 ++++++++++++++++++++++++-- src/xrpld/rpc/handlers/LedgerData.cpp | 2 +- src/xrpld/rpc/handlers/Simulate.cpp | 72 +++++++++++------ 3 files changed, 155 insertions(+), 31 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 5b3c0d23721..4343b75a9cf 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -93,18 +93,24 @@ class Simulate_test : public beast::unit_test::suite Json::Value const& tx, std::function const& validate, - bool testSerialized = true) + bool testSerialized = true, + Json::Value const& extraParams = Json::objectValue) { env.close(); - Json::Value params; + Json::Value params(extraParams); params[jss::tx_json] = tx; validate(env.rpc("json", "simulate", to_string(params)), tx); params[jss::binary] = true; validate(env.rpc("json", "simulate", to_string(params)), tx); - validate(env.rpc("simulate", to_string(tx)), tx); - validate(env.rpc("simulate", to_string(tx), "binary"), tx); + if (extraParams.size() == 0) + { + // haven't added support to CLI mode for ledger_index/ledger_hash + // yet + validate(env.rpc("simulate", to_string(tx)), tx); + validate(env.rpc("simulate", to_string(tx), "binary"), tx); + } if (testSerialized) { @@ -116,14 +122,17 @@ class Simulate_test : public beast::unit_test::suite strHex(parsed.object->getSerializer().peekData()); if (BEAST_EXPECT(parsed.object.has_value())) { - Json::Value params; + Json::Value params(extraParams); params[jss::tx_blob] = tx_blob; validate(env.rpc("json", "simulate", to_string(params)), tx); params[jss::binary] = true; validate(env.rpc("json", "simulate", to_string(params)), tx); } - validate(env.rpc("simulate", tx_blob), tx); - validate(env.rpc("simulate", tx_blob, "binary"), tx); + if (extraParams.size() == 0) + { + validate(env.rpc("simulate", tx_blob), tx); + validate(env.rpc("simulate", tx_blob, "binary"), tx); + } } BEAST_EXPECTS( @@ -1186,6 +1195,94 @@ class Simulate_test : public beast::unit_test::suite } } + void + testSuccessfulPastLedger() + { + testcase("Successful past transaction"); + + using namespace jtx; + Env env{*this}; + Account const alice{"alice"}; + env.fund(XRP(1000), alice); + env.close(); + auto const ledgerSeq = env.current()->info().seq; + auto const aliceSeq = env.seq(alice); + env.close(); + + env(pay(alice, env.master, XRP(700))); + env.close(); + + Json::Value tx = pay(alice, env.master, XRP(400)); + { + // tx should fail in the current ledger + Json::Value request = Json::objectValue; + request[jss::tx_json] = tx; + auto const result = env.rpc("json", "simulate", to_string(request)); + BEAST_EXPECTS( + result[jss::result][jss::engine_result] == + "tecUNFUNDED_PAYMENT", + result.toStyledString()); + } + + { + auto validateOutput = [&](Json::Value const& resp, + Json::Value const& tx) { + auto result = resp[jss::result]; + checkBasicReturnValidity( + result, tx, aliceSeq, env.current()->fees().base); + + BEAST_EXPECTS( + result[jss::engine_result] == "tesSUCCESS", + result.toStyledString()); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT( + result.isMember(jss::meta) || + result.isMember(jss::meta_blob))) + { + Json::Value const metadata = getJsonMetadata(result); + + // if (BEAST_EXPECT( + // metadata.isMember(sfAffectedNodes.jsonName))) + // { + // BEAST_EXPECT( + // metadata[sfAffectedNodes.jsonName].size() == 1); + // auto node = metadata[sfAffectedNodes.jsonName][0u]; + // if (BEAST_EXPECT( + // node.isMember(sfModifiedNode.jsonName))) + // { + // auto modifiedNode = node[sfModifiedNode]; + // BEAST_EXPECT( + // modifiedNode[sfLedgerEntryType] == + // "AccountRoot"); + // auto finalFields = modifiedNode[sfFinalFields]; + // BEAST_EXPECT(finalFields[sfDomain] == newDomain); + // } + // } + BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); + BEAST_EXPECT( + metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + }; + + // test with autofill + Json::Value extraParams = Json::objectValue; + extraParams[jss::ledger_index] = ledgerSeq; + testTx(env, tx, validateOutput, true, extraParams); + + tx[sfSigningPubKey] = ""; + tx[sfTxnSignature] = ""; + tx[sfSequence] = aliceSeq; + tx[sfFee] = env.current()->fees().base.jsonClipped().asString(); + + // // test without autofill + testTx(env, tx, validateOutput, true, extraParams); + } + } + public: void run() override @@ -1202,6 +1299,7 @@ class Simulate_test : public beast::unit_test::suite testMultisignedBadPubKey(); testDeleteExpiredCredentials(); testSuccessfulTransactionNetworkID(); + testSuccessfulPastLedger(); } }; diff --git a/src/xrpld/rpc/handlers/LedgerData.cpp b/src/xrpld/rpc/handlers/LedgerData.cpp index 7bd50cc1e54..9adfc239725 100644 --- a/src/xrpld/rpc/handlers/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/LedgerData.cpp @@ -61,7 +61,7 @@ doLedgerData(RPC::JsonContext& context) return RPC::expected_field_error(jss::marker, "valid"); } - bool const isBinary = params[jss::binary].asBool(); + bool const isBinary = params.get(jss::binary, false).asBool(); int limit = -1; if (params.isMember(jss::limit)) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 3c175883c5c..61104d8642b 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -35,7 +36,10 @@ namespace ripple { static Expected -getAutofillSequence(Json::Value const& tx_json, RPC::JsonContext& context) +getAutofillSequence( + Json::Value const& tx_json, + RPC::JsonContext& context, + std::shared_ptr lpLedger) { // autofill Sequence bool const hasTicketSeq = tx_json.isMember(sfTicketSequence.jsonName); @@ -54,8 +58,7 @@ getAutofillSequence(Json::Value const& tx_json, RPC::JsonContext& context) rpcSRC_ACT_MALFORMED, RPC::invalid_field_message("tx.Account"))); } std::shared_ptr const sle = - context.app.openLedger().current()->read( - keylet::account(*srcAddressID)); + lpLedger->read(keylet::account(*srcAddressID)); if (!hasTicketSeq && !sle) { JLOG(context.app.journal("Simulate").debug()) @@ -64,28 +67,45 @@ getAutofillSequence(Json::Value const& tx_json, RPC::JsonContext& context) return Unexpected(rpcError(rpcSRC_ACT_NOT_FOUND)); } + if (hasTicketSeq) + { + return 0; + } + if (!lpLedger->open()) + return sle->getFieldU32(sfSequence); - return hasTicketSeq ? 0 : context.app.getTxQ().nextQueuableSeq(sle).value(); + return context.app.getTxQ().nextQueuableSeq(sle).value(); } static std::optional -autofillTx(Json::Value& tx_json, RPC::JsonContext& context) +autofillTx( + Json::Value& tx_json, + RPC::JsonContext& context, + std::shared_ptr lpLedger) { if (!tx_json.isMember(jss::Fee)) { // autofill Fee // Must happen after all the other autofills happen // Error handling/messaging works better that way - auto feeOrError = RPC::getCurrentNetworkFee( - context.role, - context.app.config(), - context.app.getFeeTrack(), - context.app.getTxQ(), - context.app, - tx_json); - if (feeOrError.isMember(jss::error)) - return feeOrError; - tx_json[jss::Fee] = feeOrError; + if (lpLedger->open()) + { + auto feeOrError = RPC::getCurrentNetworkFee( + context.role, + context.app.config(), + context.app.getFeeTrack(), + context.app.getTxQ(), + context.app, + tx_json); + if (feeOrError.isMember(jss::error)) + return feeOrError; + tx_json[jss::Fee] = feeOrError; + } + else + { + // can't calculate server load for a past ledger + tx_json[jss::Fee] = lpLedger->fees().base.jsonClipped(); + } } if (!tx_json.isMember(jss::SigningPubKey)) @@ -139,7 +159,7 @@ autofillTx(Json::Value& tx_json, RPC::JsonContext& context) if (!tx_json.isMember(jss::Sequence)) { - auto const seq = getAutofillSequence(tx_json, context); + auto const seq = getAutofillSequence(tx_json, context, lpLedger); if (!seq) return seq.error(); tx_json[sfSequence.jsonName] = *seq; @@ -218,11 +238,14 @@ getTxJsonFromParams(Json::Value const& params) } static Json::Value -simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) +simulateTxn( + RPC::JsonContext& context, + std::shared_ptr transaction, + std::shared_ptr lpLedger) { Json::Value jvResult; // Process the transaction - OpenView view = *context.app.openLedger().current(); + OpenView view = OpenView(&*lpLedger); auto const result = context.app.getTxQ().apply( context.app, view, @@ -298,8 +321,6 @@ doSimulate(RPC::JsonContext& context) { context.loadType = Resource::feeMediumBurdenRPC; - Json::Value tx_json; // the tx as a JSON - // check validity of `binary` param if (context.params.isMember(jss::binary) && !context.params[jss::binary].isBool()) @@ -316,13 +337,18 @@ doSimulate(RPC::JsonContext& context) } } + std::shared_ptr lpLedger; + auto jvResult = RPC::lookupLedger(lpLedger, context); + if (!lpLedger) + return jvResult; + // get JSON equivalent of transaction - tx_json = getTxJsonFromParams(context.params); + Json::Value tx_json = getTxJsonFromParams(context.params); if (tx_json.isMember(jss::error)) return tx_json; // autofill fields if they're not included (e.g. `Fee`, `Sequence`) - if (auto error = autofillTx(tx_json, context)) + if (auto error = autofillTx(tx_json, context, lpLedger)) return *error; STParsedJSONObject parsed(std::string(jss::tx_json), tx_json); @@ -352,7 +378,7 @@ doSimulate(RPC::JsonContext& context) // Actually run the transaction through the transaction processor try { - return simulateTxn(context, transaction); + return simulateTxn(context, transaction, lpLedger); } // LCOV_EXCL_START this is just in case, so rippled doesn't crash catch (std::exception const& e) From fb4b93328ad3a9cbabdb4f86287e1cbdf694e285 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 31 Jul 2025 01:17:00 -0400 Subject: [PATCH 02/32] clean up --- src/xrpld/rpc/handlers/Simulate.cpp | 57 ++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 61104d8642b..f12d7fc4d8a 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -39,7 +39,8 @@ static Expected getAutofillSequence( Json::Value const& tx_json, RPC::JsonContext& context, - std::shared_ptr lpLedger) + std::shared_ptr lpLedger, + bool const isCurrentLedger) { // autofill Sequence bool const hasTicketSeq = tx_json.isMember(sfTicketSequence.jsonName); @@ -59,7 +60,11 @@ getAutofillSequence( } std::shared_ptr const sle = lpLedger->read(keylet::account(*srcAddressID)); - if (!hasTicketSeq && !sle) + if (hasTicketSeq) + { + return 0; + } + if (!sle) { JLOG(context.app.journal("Simulate").debug()) << "Failed to find source account " @@ -67,11 +72,7 @@ getAutofillSequence( return Unexpected(rpcError(rpcSRC_ACT_NOT_FOUND)); } - if (hasTicketSeq) - { - return 0; - } - if (!lpLedger->open()) + if (!isCurrentLedger) return sle->getFieldU32(sfSequence); return context.app.getTxQ().nextQueuableSeq(sle).value(); @@ -81,14 +82,15 @@ static std::optional autofillTx( Json::Value& tx_json, RPC::JsonContext& context, - std::shared_ptr lpLedger) + std::shared_ptr lpLedger, + bool const isCurrentLedger) { if (!tx_json.isMember(jss::Fee)) { // autofill Fee // Must happen after all the other autofills happen // Error handling/messaging works better that way - if (lpLedger->open()) + if (isCurrentLedger) { auto feeOrError = RPC::getCurrentNetworkFee( context.role, @@ -159,7 +161,8 @@ autofillTx( if (!tx_json.isMember(jss::Sequence)) { - auto const seq = getAutofillSequence(tx_json, context, lpLedger); + auto const seq = + getAutofillSequence(tx_json, context, lpLedger, isCurrentLedger); if (!seq) return seq.error(); tx_json[sfSequence.jsonName] = *seq; @@ -241,11 +244,13 @@ static Json::Value simulateTxn( RPC::JsonContext& context, std::shared_ptr transaction, - std::shared_ptr lpLedger) + std::shared_ptr lpLedger, + bool const isCurrentLedger) { Json::Value jvResult; // Process the transaction - OpenView view = OpenView(&*lpLedger); + OpenView view = isCurrentLedger ? *context.app.openLedger().current() + : OpenView(&*lpLedger); auto const result = context.app.getTxQ().apply( context.app, view, @@ -312,6 +317,29 @@ simulateTxn( return jvResult; } +bool +checkIsCurrentLedger(Json::Value const params) +{ + if (params.isMember(jss::ledger_index)) + { + auto const& ledgerIndex = params[jss::ledger_index]; + if (!ledgerIndex.isNull()) + { + if (ledgerIndex == RPC::LedgerShortcut::CURRENT) + { + return true; + } + return false; + } + } + if (params.isMember(jss::ledger_hash)) + { + if (!params[jss::ledger_hash].isNull()) + return false; + } + return true; +} + // { // tx_blob: XOR tx_json: , // binary: @@ -341,6 +369,7 @@ doSimulate(RPC::JsonContext& context) auto jvResult = RPC::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; + bool const isCurrentLedger = checkIsCurrentLedger(context.params); // get JSON equivalent of transaction Json::Value tx_json = getTxJsonFromParams(context.params); @@ -348,7 +377,7 @@ doSimulate(RPC::JsonContext& context) return tx_json; // autofill fields if they're not included (e.g. `Fee`, `Sequence`) - if (auto error = autofillTx(tx_json, context, lpLedger)) + if (auto error = autofillTx(tx_json, context, lpLedger, isCurrentLedger)) return *error; STParsedJSONObject parsed(std::string(jss::tx_json), tx_json); @@ -378,7 +407,7 @@ doSimulate(RPC::JsonContext& context) // Actually run the transaction through the transaction processor try { - return simulateTxn(context, transaction, lpLedger); + return simulateTxn(context, transaction, lpLedger, isCurrentLedger); } // LCOV_EXCL_START this is just in case, so rippled doesn't crash catch (std::exception const& e) From 7e16bc5005d31ee70d4bfc0f77e2cdc0d335397e Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 31 Jul 2025 09:25:44 -0400 Subject: [PATCH 03/32] improve test --- src/test/rpc/Simulate_test.cpp | 48 +++++++++++++++++++---------- src/xrpld/rpc/handlers/Simulate.cpp | 6 +--- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 4343b75a9cf..308c973643f 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -1245,23 +1245,37 @@ class Simulate_test : public beast::unit_test::suite { Json::Value const metadata = getJsonMetadata(result); - // if (BEAST_EXPECT( - // metadata.isMember(sfAffectedNodes.jsonName))) - // { - // BEAST_EXPECT( - // metadata[sfAffectedNodes.jsonName].size() == 1); - // auto node = metadata[sfAffectedNodes.jsonName][0u]; - // if (BEAST_EXPECT( - // node.isMember(sfModifiedNode.jsonName))) - // { - // auto modifiedNode = node[sfModifiedNode]; - // BEAST_EXPECT( - // modifiedNode[sfLedgerEntryType] == - // "AccountRoot"); - // auto finalFields = modifiedNode[sfFinalFields]; - // BEAST_EXPECT(finalFields[sfDomain] == newDomain); - // } - // } + if (BEAST_EXPECT( + metadata.isMember(sfAffectedNodes.jsonName))) + { + BEAST_EXPECT( + metadata[sfAffectedNodes.jsonName].size() == 2); + auto const masterMode = + metadata[sfAffectedNodes.jsonName][0u]; + if (BEAST_EXPECT( + masterMode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = masterMode[sfModifiedNode]; + BEAST_EXPECT( + modifiedNode[sfLedgerEntryType] == + "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT( + finalFields[sfBalance] == "99999999399999980"); + } + auto const aliceNode = + metadata[sfAffectedNodes.jsonName][1u]; + if (BEAST_EXPECT( + aliceNode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = aliceNode[sfModifiedNode]; + BEAST_EXPECT( + modifiedNode[sfLedgerEntryType] == + "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT(finalFields[sfBalance] == "599999990"); + } + } BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); BEAST_EXPECT( metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index f12d7fc4d8a..faabf47ed96 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -325,11 +325,7 @@ checkIsCurrentLedger(Json::Value const params) auto const& ledgerIndex = params[jss::ledger_index]; if (!ledgerIndex.isNull()) { - if (ledgerIndex == RPC::LedgerShortcut::CURRENT) - { - return true; - } - return false; + return ledgerIndex == RPC::LedgerShortcut::CURRENT; } } if (params.isMember(jss::ledger_hash)) From e377a0d832680d48fb2ed2f2f0461ae49f599039 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 31 Jul 2025 09:49:20 -0400 Subject: [PATCH 04/32] update changelog --- API-CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index dd3fcd018bb..8a0eae92cc4 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -83,6 +83,12 @@ The [commandline](https://xrpl.org/docs/references/http-websocket-apis/api-conve The `network_id` field was added in the `server_info` response in version 1.5.0 (2019), but it is not returned in [reporting mode](https://xrpl.org/rippled-server-modes.html#reporting-mode). However, use of reporting mode is now discouraged, in favor of using [Clio](https://github.com/XRPLF/clio) instead. +## Unreleased + +### Additions and bugfixes + +- `simulate`: There is now additional support for simulating transactions in past ledgers, by providing the `ledger_index` or `ledger_hash`. + ## XRP Ledger server version 2.5.0 As of 2025-04-04, version 2.5.0 is in development. You can use a pre-release version by building from source or [using the `nightly` package](https://xrpl.org/docs/infrastructure/installation/install-rippled-on-ubuntu). From f84950a41fd0b11f5493021ca487280744daadc5 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 31 Jul 2025 16:33:06 -0400 Subject: [PATCH 05/32] fix tests --- src/test/rpc/Simulate_test.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 308c973643f..4f8921421ea 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -1261,7 +1261,10 @@ class Simulate_test : public beast::unit_test::suite "AccountRoot"); auto finalFields = modifiedNode[sfFinalFields]; BEAST_EXPECT( - finalFields[sfBalance] == "99999999399999980"); + finalFields[sfBalance] == + (XRP(99999999400) - + env.current()->fees().base * 2) + .getJson()); } auto const aliceNode = metadata[sfAffectedNodes.jsonName][1u]; @@ -1273,7 +1276,10 @@ class Simulate_test : public beast::unit_test::suite modifiedNode[sfLedgerEntryType] == "AccountRoot"); auto finalFields = modifiedNode[sfFinalFields]; - BEAST_EXPECT(finalFields[sfBalance] == "599999990"); + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(600) - env.current()->fees().base) + .getJson()); } } BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); From b92ca51a81e2adfc0cdc819f4068dd2eb60a1b04 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 31 Jul 2025 17:39:26 -0400 Subject: [PATCH 06/32] add tx_hash and ctid support --- include/xrpl/protocol/jss.h | 2 +- src/test/rpc/Simulate_test.cpp | 42 ++++++++--- src/xrpld/rpc/handlers/Simulate.cpp | 107 ++++++++++++++++++++++++++-- 3 files changed, 136 insertions(+), 15 deletions(-) diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 67a045fa58d..3582359c2f1 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -637,7 +637,7 @@ JSS(trusted_validator_keys); // out: ValidatorList JSS(tx); // out: STTx, AccountTx* JSS(tx_blob); // in/out: Submit, // in: TransactionSign, AccountTx* -JSS(tx_hash); // in: TransactionEntry +JSS(tx_hash); // in: TransactionEntry, simulate JSS(tx_json); // in/out: TransactionSign // out: TransactionEntry JSS(tx_signing_hash); // out: TransactionSign diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 4f8921421ea..85c7393f165 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -167,9 +167,11 @@ class Simulate_test : public beast::unit_test::suite // No params Json::Value const params = Json::objectValue; auto const resp = env.rpc("json", "simulate", to_string(params)); - BEAST_EXPECT( + BEAST_EXPECTS( resp[jss::result][jss::error_message] == - "Neither `tx_blob` nor `tx_json` included."); + "None of `tx_blob`, `tx_json`, `tx_hash`, or `ctid` " + "included.", + resp.toStyledString()); } { // Providing both `tx_json` and `tx_blob` @@ -1203,16 +1205,20 @@ class Simulate_test : public beast::unit_test::suite using namespace jtx; Env env{*this}; Account const alice{"alice"}; + auto const netID = env.app().config().NETWORK_ID; env.fund(XRP(1000), alice); env.close(); + auto const ledgerSeq = env.current()->info().seq; auto const aliceSeq = env.seq(alice); env.close(); - env(pay(alice, env.master, XRP(700))); + Json::Value tx = pay(alice, env.master, XRP(700)); + env(tx); + auto const txHash = to_string(env.tx()->getTransactionID()); + auto const ctid = *RPC::encodeCTID(env.current()->info().seq, 0, netID); env.close(); - Json::Value tx = pay(alice, env.master, XRP(400)); { // tx should fail in the current ledger Json::Value request = Json::objectValue; @@ -1260,11 +1266,12 @@ class Simulate_test : public beast::unit_test::suite modifiedNode[sfLedgerEntryType] == "AccountRoot"); auto finalFields = modifiedNode[sfFinalFields]; - BEAST_EXPECT( + BEAST_EXPECTS( finalFields[sfBalance] == - (XRP(99999999400) - - env.current()->fees().base * 2) - .getJson()); + (XRP(99999999700) - + env.current()->fees().base * 2) + .getJson(), + metadata.toStyledString()); } auto const aliceNode = metadata[sfAffectedNodes.jsonName][1u]; @@ -1278,7 +1285,7 @@ class Simulate_test : public beast::unit_test::suite auto finalFields = modifiedNode[sfFinalFields]; BEAST_EXPECT( finalFields[sfBalance] == - (XRP(600) - env.current()->fees().base) + (XRP(300) - env.current()->fees().base) .getJson()); } } @@ -1298,8 +1305,23 @@ class Simulate_test : public beast::unit_test::suite tx[sfSequence] = aliceSeq; tx[sfFee] = env.current()->fees().base.jsonClipped().asString(); - // // test without autofill + // test without autofill testTx(env, tx, validateOutput, true, extraParams); + + // test with hash + { + Json::Value params(extraParams); + params[jss::tx_hash] = txHash; + validateOutput( + env.rpc("json", "simulate", to_string(params)), tx); + } + // test with ctid + { + Json::Value params(extraParams); + params[jss::ctid] = ctid; + validateOutput( + env.rpc("json", "simulate", to_string(params)), tx); + } } } diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index faabf47ed96..23fc20a4f01 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -19,10 +19,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -179,8 +181,98 @@ autofillTx( } static Json::Value -getTxJsonFromParams(Json::Value const& params) +getTxJsonFromHistory(RPC::JsonContext& context, bool const isCurrentLedger) { + auto const params = context.params; + uint256 hash; + if (params.isMember(jss::tx_hash)) + { + auto const tx_hash = params[jss::tx_hash]; + if (!tx_hash.isString()) + { + return RPC::invalid_field_error(jss::tx_hash); + } + if (isCurrentLedger) + { + return RPC::make_param_error( + "Cannot use `tx_hash` without `ledger_index` or " + "`ledger_hash`."); + } + if (!hash.parseHex(context.params[jss::tx_hash].asString())) + return RPC::invalid_field_error(jss::tx_hash); + } + else if (params.isMember(jss::ctid)) + { + auto const ctid = params[jss::ctid]; + if (!ctid.isString()) + { + return RPC::invalid_field_error(jss::ctid); + } + if (isCurrentLedger) + { + return RPC::make_param_error( + "Cannot use `ctid` without `ledger_index` or `ledger_hash`."); + } + auto decodedCTID = + RPC::decodeCTID(context.params[jss::ctid].asString()); + auto const [lgr_seq, txn_idx, net_id] = *decodedCTID; + if (!ctid) + { + return RPC::invalid_field_error(jss::ctid); + } + if (auto const optHash = + context.app.getLedgerMaster().txnIdFromIndex(lgr_seq, txn_idx); + optHash) + { + hash = *optHash; + } + else + { + return RPC::make_error(rpcTXN_NOT_FOUND); + } + } + if (!hash) + { + return RPC::make_param_error( + "None of `tx_blob`, `tx_json`, `tx_hash`, or `ctid` included."); + } + using TxPair = + std::pair, std::shared_ptr>; + auto ec{rpcSUCCESS}; + std::variant v = + context.app.getMasterTransaction().fetch(hash, ec); + if (auto e = std::get_if(&v)) + { + return RPC::make_error(rpcTXN_NOT_FOUND); + } + + auto [txn, _meta] = std::get(v); + Json::Value tx_json = txn->getJson(JsonOptions::none); + for (auto const field : + {jss::SigningPubKey, + jss::TxnSignature, + jss::ctid, + jss::hash, + jss::inLedger, + jss::ledger_index}) + { + if (tx_json.isMember(field)) + tx_json.removeMember(field); + } + if (tx_json.isMember(jss::Signers)) + { + for (auto& signer : tx_json[jss::Signers]) + { + signer[jss::Signer].removeMember(jss::TxnSignature); + } + } + return tx_json; +} + +static Json::Value +getTxJsonFromParams(RPC::JsonContext& context, bool const isCurrentLedger) +{ + auto const params = context.params; Json::Value tx_json; if (params.isMember(jss::tx_blob)) @@ -222,8 +314,15 @@ getTxJsonFromParams(Json::Value const& params) } else { - return RPC::make_param_error( - "Neither `tx_blob` nor `tx_json` included."); + auto const result = getTxJsonFromHistory(context, isCurrentLedger); + if (result.isMember(jss::error)) + { + return result; + } + else + { + tx_json = result; + } } // basic sanity checks for transaction shape @@ -368,7 +467,7 @@ doSimulate(RPC::JsonContext& context) bool const isCurrentLedger = checkIsCurrentLedger(context.params); // get JSON equivalent of transaction - Json::Value tx_json = getTxJsonFromParams(context.params); + Json::Value tx_json = getTxJsonFromParams(context, isCurrentLedger); if (tx_json.isMember(jss::error)) return tx_json; From 7110bf1e1d9614b4bb1d68c463e9b692499c74e6 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 31 Jul 2025 18:55:17 -0400 Subject: [PATCH 07/32] fix build issue --- src/xrpld/rpc/handlers/Simulate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 23fc20a4f01..c208732f160 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -241,7 +241,7 @@ getTxJsonFromHistory(RPC::JsonContext& context, bool const isCurrentLedger) auto ec{rpcSUCCESS}; std::variant v = context.app.getMasterTransaction().fetch(hash, ec); - if (auto e = std::get_if(&v)) + if (std::get_if(&v)) { return RPC::make_error(rpcTXN_NOT_FOUND); } From 20cb0ce5522ec3a7d8e5e20be7dabf18e9a9115a Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 1 Aug 2025 12:48:35 -0400 Subject: [PATCH 08/32] add more tests, refactor --- src/test/rpc/Simulate_test.cpp | 107 ++++++++++++++++++++++++++++ src/xrpld/rpc/detail/RPCHelpers.cpp | 97 +++++++++++++++++-------- 2 files changed, 176 insertions(+), 28 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 85c7393f165..f88b0c85279 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -438,6 +438,113 @@ class Simulate_test : public beast::unit_test::suite resp[jss::result][jss::error_message] == "Transaction should not be signed."); } + { + // non-string CTID + Json::Value params; + params[jss::ctid] = 1; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'ctid'."); + BEAST_EXPECT( + resp[jss::result][jss::error_code] == rpcINVALID_PARAMS); + } + // non-string tx hash + { + // Non-string tx_hash + Json::Value params; + params[jss::tx_hash] = 12345; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'tx_hash'."); + BEAST_EXPECT( + resp[jss::result][jss::error_code] == rpcINVALID_PARAMS); + } + { + // tx_hash with missing ledger_index/ledger_hash (current ledger) + Json::Value params; + params[jss::tx_hash] = "ABCDEF1234567890"; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot use `tx_hash` without `ledger_index` or " + "`ledger_hash`."); + } + { + // ctid with missing ledger_index/ledger_hash (current ledger) + Json::Value params; + params[jss::ctid] = "ABCDEF1234567890"; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot use `ctid` without `ledger_index` or `ledger_hash`."); + } + { + // tx_hash not found + Json::Value params; + params[jss::tx_hash] = std::string(64, 'A'); + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECTS( + resp[jss::result][jss::error_message] == + "Transaction not found.", + resp.toStyledString()); + BEAST_EXPECT( + resp[jss::result][jss::error_code] == rpcTXN_NOT_FOUND); + } + { + // ctid not found + Json::Value params; + params[jss::ctid] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Transaction not found."); + BEAST_EXPECT( + resp[jss::result][jss::error_code] == rpcTXN_NOT_FOUND); + } + { + // Unknown field in tx_json + Json::Value params; + Json::Value tx_json = Json::objectValue; + tx_json[jss::TransactionType] = jss::AccountSet; + tx_json[jss::Account] = env.master.human(); + tx_json["UnknownField"] = "value"; + params[jss::tx_json] = tx_json; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Field 'tx_json.UnknownField' is unknown."); + } + { + // TicketSequence autofill returns 0 + Json::Value params; + Json::Value tx_json = Json::objectValue; + tx_json[jss::TransactionType] = jss::AccountSet; + tx_json[jss::Account] = env.master.human(); + tx_json[sfTicketSequence.jsonName] = 1; + params[jss::tx_json] = tx_json; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::tx_json][sfSequence.jsonName] == 0); + } + { + // Invalid ledger_index type + Json::Value params; + Json::Value tx_json = Json::objectValue; + tx_json[jss::TransactionType] = jss::AccountSet; + tx_json[jss::Account] = env.master.human(); + params[jss::tx_json] = tx_json; + params[jss::ledger_index] = Json::arrayValue; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'ledger_index'."); + } } void diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index b98f31340ae..cace44121c8 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -354,36 +354,18 @@ isValidatedOld(LedgerMaster& ledgerMaster, bool standalone) template Status -ledgerFromRequest(T& ledger, JsonContext& context) +ledgerFromHash(T& ledger, Json::Value hash, JsonContext& context) { - ledger.reset(); - - auto& params = context.params; - - auto indexValue = params[jss::ledger_index]; - auto hashValue = params[jss::ledger_hash]; - - // We need to support the legacy "ledger" field. - auto& legacyLedger = params[jss::ledger]; - if (legacyLedger) - { - if (legacyLedger.asString().size() > 12) - hashValue = legacyLedger; - else - indexValue = legacyLedger; - } - - if (hashValue) - { - if (!hashValue.isString()) - return {rpcINVALID_PARAMS, "ledgerHashNotString"}; - - uint256 ledgerHash; - if (!ledgerHash.parseHex(hashValue.asString())) - return {rpcINVALID_PARAMS, "ledgerHashMalformed"}; - return getLedger(ledger, ledgerHash, context); - } + uint256 ledgerHash; + if (!ledgerHash.parseHex(hash.asString())) + return {rpcINVALID_PARAMS, "ledgerHashMalformed"}; + return getLedger(ledger, ledgerHash, context); +} +template +Status +ledgerFromIndex(T& ledger, Json::Value indexValue, JsonContext& context) +{ auto const index = indexValue.asString(); if (index == "current" || index.empty()) @@ -401,6 +383,65 @@ ledgerFromRequest(T& ledger, JsonContext& context) return {rpcINVALID_PARAMS, "ledgerIndexMalformed"}; } + +template +Status +ledgerFromRequest(T& ledger, JsonContext& context) +{ + ledger.reset(); + + auto& params = context.params; + auto const hasLedger = context.params.isMember(jss::ledger); + auto const hasHash = context.params.isMember(jss::ledger_hash); + auto const hasIndex = context.params.isMember(jss::ledger_index); + + if ((hasLedger + hasHash + hasIndex) > 1) + { + return { + rpcINVALID_PARAMS, + "Only one of `ledger`, `ledger_hash`, or " + "`ledger_index` can be specified."}; + } + + // We need to support the legacy "ledger" field. + if (hasLedger) + { + auto& legacyLedger = params[jss::ledger]; + if (!legacyLedger.isString() && !legacyLedger.isUInt() && + !legacyLedger.isInt()) + { + return {rpcINVALID_PARAMS, invalid_field_message(jss::ledger)}; + } + if (legacyLedger.asString().size() > 12) + return ledgerFromHash(ledger, legacyLedger, context); + else + return ledgerFromIndex(ledger, legacyLedger, context); + } + + if (hasHash) + { + auto const& ledgerHash = params[jss::ledger_hash]; + if (!ledgerHash.isString()) + return {rpcINVALID_PARAMS, invalid_field_message(jss::ledger_hash)}; + return ledgerFromHash(ledger, ledgerHash, context); + } + + if (hasIndex) + { + auto const& ledgerIndex = params[jss::ledger_index]; + if (!ledgerIndex.isString() && !ledgerIndex.isUInt() && + !ledgerIndex.isInt()) + { + return { + rpcINVALID_PARAMS, invalid_field_message(jss::ledger_index)}; + } + return ledgerFromIndex(ledger, ledgerIndex, context); + } + + // nothing specified, `index` has a default setting + // TODO: more cleanup in this file needed + return ledgerFromIndex(ledger, Json::nullValue, context); +} } // namespace template From 10a6a449ddf704b10b671cae7c57bba1e1a5b7cc Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 1 Aug 2025 15:01:52 -0400 Subject: [PATCH 09/32] fix tests --- src/test/rpc/AccountCurrencies_test.cpp | 7 +- src/test/rpc/AccountLines_test.cpp | 96 ++++++++++++------------- src/test/rpc/LedgerRPC_test.cpp | 95 +++++++----------------- src/test/rpc/NoRippleCheck_test.cpp | 3 +- src/test/rpc/Simulate_test.cpp | 13 ++++ src/xrpld/rpc/detail/RPCHelpers.cpp | 11 ++- 6 files changed, 99 insertions(+), 126 deletions(-) diff --git a/src/test/rpc/AccountCurrencies_test.cpp b/src/test/rpc/AccountCurrencies_test.cpp index 3ccb89c471f..d93f11ed4b9 100644 --- a/src/test/rpc/AccountCurrencies_test.cpp +++ b/src/test/rpc/AccountCurrencies_test.cpp @@ -43,11 +43,10 @@ class AccountCurrencies_test : public beast::unit_test::suite params[jss::account] = Account{"bob"}.human(); params[jss::ledger_hash] = 1; auto const result = env.rpc( - "json", - "account_currencies", - boost::lexical_cast(params))[jss::result]; + "json", "account_currencies", to_string(params))[jss::result]; BEAST_EXPECT(result[jss::error] == "invalidParams"); - BEAST_EXPECT(result[jss::error_message] == "ledgerHashNotString"); + BEAST_EXPECT( + result[jss::error_message] == "Invalid field 'ledger_hash'."); } { // missing account field diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index 9215f4087a9..66b13e180c1 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -183,26 +183,27 @@ class AccountLines_test : public beast::unit_test::suite LedgerInfo const& info, int count) { // Get account_lines by ledger index. - auto const linesSeq = env.rpc( - "json", - "account_lines", - R"({"account": ")" + account.human() + - R"(", )" - R"("ledger_index": )" + - std::to_string(info.seq) + "}"); - BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray()); - BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count); + { + Json::Value params; + params[jss::account] = account.human(); + params[jss::ledger_index] = info.seq; + auto const linesSeq = + env.rpc("json", "account_lines", to_string(params)); + BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray()); + BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count); + } // Get account_lines by ledger hash. - auto const linesHash = env.rpc( - "json", - "account_lines", - R"({"account": ")" + account.human() + - R"(", )" - R"("ledger_hash": ")" + - to_string(info.hash) + R"("})"); - BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray()); - BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count); + { + Json::Value params; + params[jss::account] = account.human(); + params[jss::ledger_hash] = to_string(info.hash); + auto const linesHash = + env.rpc("json", "account_lines", to_string(params)); + BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray()); + BEAST_EXPECT( + linesHash[jss::result][jss::lines].size() == count); + } }; // Alice should have no trust lines in ledger 3. @@ -217,18 +218,17 @@ class AccountLines_test : public beast::unit_test::suite { // Surprisingly, it's valid to specify both index and hash, in // which case the hash wins. + Json::Value params; + params[jss::account] = alice.human(); + params[jss::ledger_hash] = to_string(ledger4Info.hash); + params[jss::ledger_index] = ledger58Info.seq; auto const lines = env.rpc( - "json", - "account_lines", - R"({"account": ")" + alice.human() + - R"(", )" - R"("ledger_hash": ")" + - to_string(ledger4Info.hash) + - R"(", )" - R"("ledger_index": )" + - std::to_string(ledger58Info.seq) + "}"); - BEAST_EXPECT(lines[jss::result][jss::lines].isArray()); - BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26); + "json", "account_lines", to_string(params))[jss::result]; + BEAST_EXPECT(lines[jss::error] == "invalidParams"); + BEAST_EXPECT( + lines[jss::error_message] == + "Exactly one of 'ledger_hash' or 'ledger_index' can be " + "specified."); } { // alice should have 52 trust lines in the current ledger. @@ -1051,26 +1051,24 @@ class AccountLines_test : public beast::unit_test::suite testAccountLinesHistory(alice, ledger58Info, 52); { - // Surprisingly, it's valid to specify both index and hash, in - // which case the hash wins. - auto const lines = env.rpc( - "json2", - "{ " - R"("method" : "account_lines",)" - R"("jsonrpc" : "2.0",)" - R"("ripplerpc" : "2.0",)" - R"("id" : 5,)" - R"("params": )" - R"({"account": ")" + - alice.human() + - R"(", )" - R"("ledger_hash": ")" + - to_string(ledger4Info.hash) + - R"(", )" - R"("ledger_index": )" + - std::to_string(ledger58Info.seq) + "}}"); - BEAST_EXPECT(lines[jss::result][jss::lines].isArray()); - BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26); + Json::Value params; + params[jss::method] = "account_lines"; + params[jss::jsonrpc] = "2.0"; + params[jss::ripplerpc] = "2.0"; + params[jss::id] = 5; + { + Json::Value subParams; + subParams[jss::account] = alice.human(); + subParams[jss::ledger_hash] = to_string(ledger4Info.hash); + subParams[jss::ledger_index] = ledger58Info.seq; + params[jss::params] = subParams; + } + auto const lines = env.rpc("json2", to_string(params)); + BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams"); + BEAST_EXPECT( + lines[jss::error][jss::message] == + "Exactly one of 'ledger_hash' or 'ledger_index' can be " + "specified."); BEAST_EXPECT( lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); BEAST_EXPECT( diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 9ba9c9a6555..1fcec6f22ff 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -287,56 +287,39 @@ class LedgerRPC_test : public beast::unit_test::suite // access via the legacy ledger field, keyword index values Json::Value jvParams; jvParams[jss::ledger] = "closed"; - auto jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + auto jrr = + env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::ledger)); BEAST_EXPECT(jrr.isMember(jss::ledger_hash)); BEAST_EXPECT(jrr[jss::ledger][jss::ledger_index] == "5"); jvParams[jss::ledger] = "validated"; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::ledger)); BEAST_EXPECT(jrr.isMember(jss::ledger_hash)); BEAST_EXPECT(jrr[jss::ledger][jss::ledger_index] == "5"); jvParams[jss::ledger] = "current"; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::ledger)); BEAST_EXPECT(jrr[jss::ledger][jss::ledger_index] == "6"); // ask for a bad ledger keyword jvParams[jss::ledger] = "invalid"; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "invalidParams"); BEAST_EXPECT(jrr[jss::error_message] == "ledgerIndexMalformed"); // numeric index jvParams[jss::ledger] = 4; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::ledger)); BEAST_EXPECT(jrr.isMember(jss::ledger_hash)); BEAST_EXPECT(jrr[jss::ledger][jss::ledger_index] == "4"); // numeric index - out of range jvParams[jss::ledger] = 20; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "lgrNotFound"); BEAST_EXPECT(jrr[jss::error_message] == "ledgerNotFound"); } @@ -348,40 +331,31 @@ class LedgerRPC_test : public beast::unit_test::suite // access via the ledger_hash field Json::Value jvParams; jvParams[jss::ledger_hash] = hash3; - auto jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + auto jrr = + env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::ledger)); BEAST_EXPECT(jrr.isMember(jss::ledger_hash)); BEAST_EXPECT(jrr[jss::ledger][jss::ledger_index] == "3"); // extra leading hex chars in hash are not allowed jvParams[jss::ledger_hash] = "DEADBEEF" + hash3; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "invalidParams"); - BEAST_EXPECT(jrr[jss::error_message] == "ledgerHashMalformed"); + BEAST_EXPECT( + jrr[jss::error_message] == "Invalid field `ledger_hash`."); // request with non-string ledger_hash jvParams[jss::ledger_hash] = 2; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "invalidParams"); - BEAST_EXPECT(jrr[jss::error_message] == "ledgerHashNotString"); + BEAST_EXPECT( + jrr[jss::error_message] == "Invalid field `ledger_hash`."); // malformed (non hex) hash jvParams[jss::ledger_hash] = "2E81FC6EC0DD943197EGC7E3FBE9AE30" "7F2775F2F7485BB37307984C3C0F2340"; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "invalidParams"); BEAST_EXPECT(jrr[jss::error_message] == "ledgerHashMalformed"); @@ -389,10 +363,7 @@ class LedgerRPC_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = "8C3EEDB3124D92E49E75D81A8826A2E6" "5A75FD71FC3FD6F36FEB803C5F1D812D"; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "lgrNotFound"); BEAST_EXPECT(jrr[jss::error_message] == "ledgerNotFound"); } @@ -401,39 +372,28 @@ class LedgerRPC_test : public beast::unit_test::suite // access via the ledger_index field, keyword index values Json::Value jvParams; jvParams[jss::ledger_index] = "closed"; - auto jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + auto jrr = + env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::ledger)); BEAST_EXPECT(jrr.isMember(jss::ledger_hash)); BEAST_EXPECT(jrr[jss::ledger][jss::ledger_index] == "5"); BEAST_EXPECT(jrr.isMember(jss::ledger_index)); jvParams[jss::ledger_index] = "validated"; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::ledger)); BEAST_EXPECT(jrr.isMember(jss::ledger_hash)); BEAST_EXPECT(jrr[jss::ledger][jss::ledger_index] == "5"); jvParams[jss::ledger_index] = "current"; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::ledger)); BEAST_EXPECT(jrr[jss::ledger][jss::ledger_index] == "6"); BEAST_EXPECT(jrr.isMember(jss::ledger_current_index)); // ask for a bad ledger keyword jvParams[jss::ledger_index] = "invalid"; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "invalidParams"); BEAST_EXPECT(jrr[jss::error_message] == "ledgerIndexMalformed"); @@ -441,10 +401,8 @@ class LedgerRPC_test : public beast::unit_test::suite for (auto i : {1, 2, 3, 4, 5, 6}) { jvParams[jss::ledger_index] = i; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = + env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::ledger)); if (i < 6) BEAST_EXPECT(jrr.isMember(jss::ledger_hash)); @@ -454,10 +412,7 @@ class LedgerRPC_test : public beast::unit_test::suite // numeric index - out of range jvParams[jss::ledger_index] = 7; - jrr = env.rpc( - "json", - "ledger", - boost::lexical_cast(jvParams))[jss::result]; + jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "lgrNotFound"); BEAST_EXPECT(jrr[jss::error_message] == "ledgerNotFound"); } diff --git a/src/test/rpc/NoRippleCheck_test.cpp b/src/test/rpc/NoRippleCheck_test.cpp index 6cd566e144a..826bda281ab 100644 --- a/src/test/rpc/NoRippleCheck_test.cpp +++ b/src/test/rpc/NoRippleCheck_test.cpp @@ -124,7 +124,8 @@ class NoRippleCheck_test : public beast::unit_test::suite "noripple_check", boost::lexical_cast(params))[jss::result]; BEAST_EXPECT(result[jss::error] == "invalidParams"); - BEAST_EXPECT(result[jss::error_message] == "ledgerHashNotString"); + BEAST_EXPECT( + result[jss::error_message] == "Invalid field 'ledger_hash'."); } { // account not found diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index f88b0c85279..ac2a59185af 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -482,6 +482,19 @@ class Simulate_test : public beast::unit_test::suite resp[jss::result][jss::error_message] == "Cannot use `ctid` without `ledger_index` or `ledger_hash`."); } + { + // invalid ledger_hash + Json::Value params; + params[jss::tx_hash] = "ABCDEF"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECTS( + resp[jss::result][jss::error_message] == + "Invalid field 'tx_hash'.", + resp.toStyledString()); + BEAST_EXPECT( + resp[jss::result][jss::error_code] == rpcINVALID_PARAMS); + } { // tx_hash not found Json::Value params; diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index cace44121c8..9fab4c560d0 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -397,10 +397,17 @@ ledgerFromRequest(T& ledger, JsonContext& context) if ((hasLedger + hasHash + hasIndex) > 1) { + // while `ledger` is still supported, it is deprecated + // and therefore shouldn't be mentioned in the error message + if (hasLedger) + return { + rpcINVALID_PARAMS, + "Exactly one of 'ledger', 'ledger_hash', or " + "'ledger_index' can be specified."}; return { rpcINVALID_PARAMS, - "Only one of `ledger`, `ledger_hash`, or " - "`ledger_index` can be specified."}; + "Exactly one of 'ledger_hash' or " + "'ledger_index' can be specified."}; } // We need to support the legacy "ledger" field. From c5a31da4ee3285fc21a83223a269c0b7a4f525ba Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 1 Aug 2025 15:23:24 -0400 Subject: [PATCH 10/32] first attempt at multi transaction processing (not tested, but doesn't break anything) --- src/test/rpc/Simulate_test.cpp | 3 +- src/xrpld/rpc/handlers/Simulate.cpp | 255 ++++++++++++++++++---------- 2 files changed, 165 insertions(+), 93 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index ac2a59185af..ad8bb37ed7f 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -182,7 +182,8 @@ class Simulate_test : public beast::unit_test::suite auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( resp[jss::result][jss::error_message] == - "Can only include one of `tx_blob` and `tx_json`."); + "Cannot include 'tx_blob' with 'tx_json', 'tx_hash', or " + "'ctid'."); } { // `binary` isn't a boolean diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index c208732f160..714905f2704 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -187,6 +187,13 @@ getTxJsonFromHistory(RPC::JsonContext& context, bool const isCurrentLedger) uint256 hash; if (params.isMember(jss::tx_hash)) { + if (params.isMember(jss::tx_blob) || params.isMember(jss::tx_json) || + params.isMember(jss::ctid)) + { + return RPC::make_param_error( + "Cannot include 'tx_hash' with 'ctid'."); + } + auto const tx_hash = params[jss::tx_hash]; if (!tx_hash.isString()) { @@ -270,20 +277,24 @@ getTxJsonFromHistory(RPC::JsonContext& context, bool const isCurrentLedger) } static Json::Value -getTxJsonFromParams(RPC::JsonContext& context, bool const isCurrentLedger) +getTxJsonFromParams( + RPC::JsonContext& context, + Json::Value txInput, + bool const isCurrentLedger) { - auto const params = context.params; Json::Value tx_json; - if (params.isMember(jss::tx_blob)) + if (txInput.isMember(jss::tx_blob)) { - if (params.isMember(jss::tx_json)) + if (txInput.isMember(jss::tx_json) || txInput.isMember(jss::tx_hash) || + txInput.isMember(jss::ctid)) { return RPC::make_param_error( - "Can only include one of `tx_blob` and `tx_json`."); + "Cannot include 'tx_blob' with 'tx_json', 'tx_hash', or " + "'ctid'."); } - auto const tx_blob = params[jss::tx_blob]; + auto const tx_blob = txInput[jss::tx_blob]; if (!tx_blob.isString()) { return RPC::invalid_field_error(jss::tx_blob); @@ -304,9 +315,15 @@ getTxJsonFromParams(RPC::JsonContext& context, bool const isCurrentLedger) return RPC::invalid_field_error(jss::tx_blob); } } - else if (params.isMember(jss::tx_json)) + else if (txInput.isMember(jss::tx_json)) { - tx_json = params[jss::tx_json]; + if (txInput.isMember(jss::tx_hash) || txInput.isMember(jss::ctid)) + { + return RPC::make_param_error( + "Cannot include 'tx_json' with 'tx_hash' or 'ctid'."); + } + + tx_json = txInput[jss::tx_json]; if (!tx_json.isObject()) { return RPC::object_field_error(jss::tx_json); @@ -342,78 +359,89 @@ getTxJsonFromParams(RPC::JsonContext& context, bool const isCurrentLedger) static Json::Value simulateTxn( RPC::JsonContext& context, - std::shared_ptr transaction, + std::vector> transactions, std::shared_ptr lpLedger, bool const isCurrentLedger) { - Json::Value jvResult; + Json::Value jvFinalResult; + jvFinalResult[jss::transactions] = Json::arrayValue; // Process the transaction OpenView view = isCurrentLedger ? *context.app.openLedger().current() : OpenView(&*lpLedger); - auto const result = context.app.getTxQ().apply( - context.app, - view, - transaction->getSTransaction(), - tapDRY_RUN, - context.j); - - jvResult[jss::applied] = result.applied; - jvResult[jss::ledger_index] = view.seq(); - - bool const isBinaryOutput = context.params.get(jss::binary, false).asBool(); + for (auto const& transaction : transactions) + { + Json::Value jvResult; + auto const result = context.app.getTxQ().apply( + context.app, + view, + transaction->getSTransaction(), + tapDRY_RUN, + context.j); + + jvResult[jss::applied] = result.applied; + jvResult[jss::ledger_index] = view.seq(); + + bool const isBinaryOutput = + context.params.get(jss::binary, false).asBool(); + + // Convert the TER to human-readable values + std::string token; + std::string message; + if (transResultInfo(result.ter, token, message)) + { + // Engine result + jvResult[jss::engine_result] = token; + jvResult[jss::engine_result_code] = result.ter; + jvResult[jss::engine_result_message] = message; + } + else + { + // shouldn't be hit + // LCOV_EXCL_START + jvResult[jss::engine_result] = "unknown"; + jvResult[jss::engine_result_code] = result.ter; + jvResult[jss::engine_result_message] = "unknown"; + // LCOV_EXCL_STOP + } - // Convert the TER to human-readable values - std::string token; - std::string message; - if (transResultInfo(result.ter, token, message)) - { - // Engine result - jvResult[jss::engine_result] = token; - jvResult[jss::engine_result_code] = result.ter; - jvResult[jss::engine_result_message] = message; - } - else - { - // shouldn't be hit - // LCOV_EXCL_START - jvResult[jss::engine_result] = "unknown"; - jvResult[jss::engine_result_code] = result.ter; - jvResult[jss::engine_result_message] = "unknown"; - // LCOV_EXCL_STOP - } + if (token == "tesSUCCESS") + { + jvResult[jss::engine_result_message] = + "The simulated transaction would have been applied."; + } - if (token == "tesSUCCESS") - { - jvResult[jss::engine_result_message] = - "The simulated transaction would have been applied."; - } + if (result.metadata) + { + if (isBinaryOutput) + { + auto const metaBlob = + result.metadata->getAsObject().getSerializer().getData(); + jvResult[jss::meta_blob] = strHex(makeSlice(metaBlob)); + } + else + { + jvResult[jss::meta] = + result.metadata->getJson(JsonOptions::none); + } + } - if (result.metadata) - { if (isBinaryOutput) { - auto const metaBlob = - result.metadata->getAsObject().getSerializer().getData(); - jvResult[jss::meta_blob] = strHex(makeSlice(metaBlob)); + auto const txBlob = + transaction->getSTransaction()->getSerializer().getData(); + jvResult[jss::tx_blob] = strHex(makeSlice(txBlob)); } else { - jvResult[jss::meta] = result.metadata->getJson(JsonOptions::none); + jvResult[jss::tx_json] = transaction->getJson(JsonOptions::none); } + jvFinalResult[jss::transactions].append(jvResult); } - - if (isBinaryOutput) - { - auto const txBlob = - transaction->getSTransaction()->getSerializer().getData(); - jvResult[jss::tx_blob] = strHex(makeSlice(txBlob)); - } - else + if (jvFinalResult[jss::transactions].size() == 1) { - jvResult[jss::tx_json] = transaction->getJson(JsonOptions::none); + jvFinalResult = jvFinalResult[jss::transactions][0u]; } - - return jvResult; + return jvFinalResult; } bool @@ -435,6 +463,49 @@ checkIsCurrentLedger(Json::Value const params) return true; } +Expected, Json::Value> +processTransaction( + RPC::JsonContext& context, + Json::Value txInput, + std::shared_ptr lpLedger, + bool const isCurrentLedger) +{ + // get JSON equivalent of transaction + Json::Value tx_json = + getTxJsonFromParams(context, txInput, isCurrentLedger); + if (tx_json.isMember(jss::error)) + return Unexpected(tx_json); + + // autofill fields if they're not included (e.g. `Fee`, `Sequence`) + if (auto error = autofillTx(tx_json, context, lpLedger, isCurrentLedger)) + return Unexpected(*error); + + STParsedJSONObject parsed(std::string(jss::tx_json), tx_json); + if (!parsed.object.has_value()) + return Unexpected(parsed.error); + + std::shared_ptr stTx; + try + { + stTx = std::make_shared(std::move(parsed.object.value())); + } + catch (std::exception& e) + { + Json::Value jvResult = Json::objectValue; + jvResult[jss::error] = "invalidTransaction"; + jvResult[jss::error_exception] = e.what(); + return Unexpected(jvResult); + } + + if (stTx->getTxnType() == ttBATCH) + { + return Unexpected(RPC::make_error(rpcNOT_IMPL)); + } + + std::string reason; + return std::make_shared(stTx, reason, context.app); +} + // { // tx_blob: XOR tx_json: , // binary: @@ -460,49 +531,49 @@ doSimulate(RPC::JsonContext& context) } } + if (context.params.isMember(jss::transactions) && + (context.params.isMember(jss::tx_json) || + context.params.isMember(jss::tx_blob) || + context.params.isMember(jss::tx_hash) || + context.params.isMember(jss::ctid))) + { + return RPC::make_param_error( + "Cannot include 'transactions' with 'tx_json', 'tx_blob', " + "'tx_hash', or 'ctid'."); + } + std::shared_ptr lpLedger; auto jvResult = RPC::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; bool const isCurrentLedger = checkIsCurrentLedger(context.params); - // get JSON equivalent of transaction - Json::Value tx_json = getTxJsonFromParams(context, isCurrentLedger); - if (tx_json.isMember(jss::error)) - return tx_json; - - // autofill fields if they're not included (e.g. `Fee`, `Sequence`) - if (auto error = autofillTx(tx_json, context, lpLedger, isCurrentLedger)) - return *error; - - STParsedJSONObject parsed(std::string(jss::tx_json), tx_json); - if (!parsed.object.has_value()) - return parsed.error; - - std::shared_ptr stTx; - try + auto transactions = std::vector>{}; + if (context.params.isMember(jss::transactions)) { - stTx = std::make_shared(std::move(parsed.object.value())); - } - catch (std::exception& e) - { - Json::Value jvResult = Json::objectValue; - jvResult[jss::error] = "invalidTransaction"; - jvResult[jss::error_exception] = e.what(); - return jvResult; + // TODO: bunch of additional checks + for (auto const& txInput : context.params[jss::transactions]) + { + auto transaction = + processTransaction(context, txInput, lpLedger, isCurrentLedger); + if (!transaction) + return transaction.error(); + transactions.push_back(transaction.value()); + } } - - if (stTx->getTxnType() == ttBATCH) + else { - return RPC::make_error(rpcNOT_IMPL); + // single transaction + auto transaction = processTransaction( + context, context.params, lpLedger, isCurrentLedger); + if (!transaction) + return transaction.error(); + transactions.push_back(transaction.value()); } - - std::string reason; - auto transaction = std::make_shared(stTx, reason, context.app); // Actually run the transaction through the transaction processor try { - return simulateTxn(context, transaction, lpLedger, isCurrentLedger); + return simulateTxn(context, transactions, lpLedger, isCurrentLedger); } // LCOV_EXCL_START this is just in case, so rippled doesn't crash catch (std::exception const& e) From 387726314db1247d89d6a4edf7f0df6f4174137e Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 1 Aug 2025 15:30:13 -0400 Subject: [PATCH 11/32] test outline (src does not work yet) --- src/test/rpc/Simulate_test.cpp | 54 ++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index ad8bb37ed7f..38e424d6adb 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -1446,23 +1446,51 @@ class Simulate_test : public beast::unit_test::suite } } + void + testMultipleTransactions() + { + testcase("Multiple transactions"); + + using namespace jtx; + Env env{*this}; + + Account const alice{"alice"}; + env.fund(XRP(1000), alice); + env.close(); + + Json::Value tx1; + tx1[jss::tx_json] = pay(alice, env.master, XRP(700)); + Json::Value tx2; + tx2[jss::tx_json] = pay(alice, env.master, XRP(200)); + + Json::Value params = Json::objectValue; + Json::Value txs = Json::arrayValue; + txs.append(tx1); + txs.append(tx2); + params[jss::transactions] = txs; + + auto const result = env.rpc("json", "simulate", to_string(params)); + std::cout << result.toStyledString() << std::endl; + } + public: void run() override { - testParamErrors(); - testFeeError(); - testInvalidTransactionType(); - testSuccessfulTransaction(); - testTransactionNonTecFailure(); - testTransactionTecFailure(); - testSuccessfulTransactionMultisigned(); - testTransactionSigningFailure(); - testInvalidSingleAndMultiSigningTransaction(); - testMultisignedBadPubKey(); - testDeleteExpiredCredentials(); - testSuccessfulTransactionNetworkID(); - testSuccessfulPastLedger(); + // testParamErrors(); + // testFeeError(); + // testInvalidTransactionType(); + // testSuccessfulTransaction(); + // testTransactionNonTecFailure(); + // testTransactionTecFailure(); + // testSuccessfulTransactionMultisigned(); + // testTransactionSigningFailure(); + // testInvalidSingleAndMultiSigningTransaction(); + // testMultisignedBadPubKey(); + // testDeleteExpiredCredentials(); + // testSuccessfulTransactionNetworkID(); + // testSuccessfulPastLedger(); + testMultipleTransactions(); } }; From 5fc32a256107b31fba80e059c80ca7caf02ee338 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 1 Aug 2025 21:25:42 -0400 Subject: [PATCH 12/32] get something working --- src/test/rpc/Simulate_test.cpp | 30 ++++++++++---------- src/xrpld/ledger/detail/ApplyStateTable.cpp | 7 ++--- src/xrpld/rpc/handlers/Simulate.cpp | 31 +++++++++++++++------ 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 38e424d6adb..a8ef01a254f 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -1459,9 +1459,11 @@ class Simulate_test : public beast::unit_test::suite env.close(); Json::Value tx1; - tx1[jss::tx_json] = pay(alice, env.master, XRP(700)); + tx1[jss::tx_json] = pay(alice, env.master, XRP(500)); Json::Value tx2; tx2[jss::tx_json] = pay(alice, env.master, XRP(200)); + // TODO: fix sequence autofilling + tx2[jss::tx_json][jss::Sequence] = env.seq(alice) + 1; Json::Value params = Json::objectValue; Json::Value txs = Json::arrayValue; @@ -1477,19 +1479,19 @@ class Simulate_test : public beast::unit_test::suite void run() override { - // testParamErrors(); - // testFeeError(); - // testInvalidTransactionType(); - // testSuccessfulTransaction(); - // testTransactionNonTecFailure(); - // testTransactionTecFailure(); - // testSuccessfulTransactionMultisigned(); - // testTransactionSigningFailure(); - // testInvalidSingleAndMultiSigningTransaction(); - // testMultisignedBadPubKey(); - // testDeleteExpiredCredentials(); - // testSuccessfulTransactionNetworkID(); - // testSuccessfulPastLedger(); + testParamErrors(); + testFeeError(); + testInvalidTransactionType(); + testSuccessfulTransaction(); + testTransactionNonTecFailure(); + testTransactionTecFailure(); + testSuccessfulTransactionMultisigned(); + testTransactionSigningFailure(); + testInvalidSingleAndMultiSigningTransaction(); + testMultisignedBadPubKey(); + testDeleteExpiredCredentials(); + testSuccessfulTransactionNetworkID(); + testSuccessfulPastLedger(); testMultipleTransactions(); } }; diff --git a/src/xrpld/ledger/detail/ApplyStateTable.cpp b/src/xrpld/ledger/detail/ApplyStateTable.cpp index 2a740093d9e..474d81e9e55 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.cpp +++ b/src/xrpld/ledger/detail/ApplyStateTable.cpp @@ -283,11 +283,8 @@ ApplyStateTable::apply( metadata = meta; } - if (!isDryRun) - { - to.rawTxInsert(tx.getTransactionID(), sTx, sMeta); - apply(to); - } + to.rawTxInsert(tx.getTransactionID(), sTx, sMeta); + apply(to); return metadata; } diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 714905f2704..830c0cd8f5f 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -365,21 +364,32 @@ simulateTxn( { Json::Value jvFinalResult; jvFinalResult[jss::transactions] = Json::arrayValue; - // Process the transaction - OpenView view = isCurrentLedger ? *context.app.openLedger().current() - : OpenView(&*lpLedger); + + OpenView origView = OpenView(&*lpLedger); + OpenView view(batch_view, origView); for (auto const& transaction : transactions) { + OpenView perTxView(batch_view, view); Json::Value jvResult; - auto const result = context.app.getTxQ().apply( + /*************************************** + * SECURITY NOTE: This technically applies the transaction to the view. + * However, since the `view` is a copy of the ledger it's being applied + * to, and is thrown away immediately after the simulated transactions + * are processed, the original ledger remains unchanged. Therefore, you + * cannot use `simulate` to bypass signature checks and submit + * transactions/modify the current ledger directly. + ***************************************/ + auto const result = apply( context.app, - view, - transaction->getSTransaction(), + perTxView, + *transaction->getSTransaction(), tapDRY_RUN, context.j); + if (isTesSuccess(result.ter) || isTecClaim(result.ter)) + perTxView.apply(view); jvResult[jss::applied] = result.applied; - jvResult[jss::ledger_index] = view.seq(); + jvResult[jss::ledger_index] = perTxView.seq(); bool const isBinaryOutput = context.params.get(jss::binary, false).asBool(); @@ -507,7 +517,10 @@ processTransaction( } // { -// tx_blob: XOR tx_json: , +// tx_blob: XOR tx_json: XOR tx_hash: XOR ctid: +// XOR +// transactions: [ ] // array of objects with one of +// tx_json/tx_blob/tx_hash/ctid, // binary: // } Json::Value From cc7585e12fd50c6de094e976c087b4640f9a9500 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 1 Aug 2025 22:12:05 -0400 Subject: [PATCH 13/32] get Batch mostly working --- src/test/rpc/Simulate_test.cpp | 63 ++++++++--- src/xrpld/app/tx/apply.h | 8 ++ src/xrpld/app/tx/detail/apply.cpp | 84 +++++++++----- src/xrpld/rpc/handlers/Simulate.cpp | 165 ++++++++++++++++------------ 4 files changed, 211 insertions(+), 109 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index a8ef01a254f..0e3c1c7702c 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -1475,24 +1475,59 @@ class Simulate_test : public beast::unit_test::suite std::cout << result.toStyledString() << std::endl; } + void + testBatchTransaction() + { + testcase("Batch transaction"); + + using namespace jtx; + Env env{*this}; + + Account const alice{"alice"}; + env.fund(XRP(1000), alice); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + Json::Value batchTx = batch::outer(alice, seq, batchFee, tfIndependent); + Json::Value tx1 = Json::objectValue; + tx1[jss::RawTransaction] = + batch::inner(pay(alice, env.master, XRP(500)), env.seq(alice) + 1) + .getTxn(); + Json::Value tx2 = Json::objectValue; + tx2[jss::RawTransaction] = + batch::inner(pay(alice, env.master, XRP(200)), env.seq(alice) + 2) + .getTxn(); + batchTx[jss::RawTransactions] = Json::arrayValue; + batchTx[jss::RawTransactions].append(tx1); + batchTx[jss::RawTransactions].append(tx2); + + Json::Value params = Json::objectValue; + params[jss::tx_json] = batchTx; + + auto const result = env.rpc("json", "simulate", to_string(params)); + std::cout << result.toStyledString() << std::endl; + } + public: void run() override { - testParamErrors(); - testFeeError(); - testInvalidTransactionType(); - testSuccessfulTransaction(); - testTransactionNonTecFailure(); - testTransactionTecFailure(); - testSuccessfulTransactionMultisigned(); - testTransactionSigningFailure(); - testInvalidSingleAndMultiSigningTransaction(); - testMultisignedBadPubKey(); - testDeleteExpiredCredentials(); - testSuccessfulTransactionNetworkID(); - testSuccessfulPastLedger(); - testMultipleTransactions(); + // testParamErrors(); + // testFeeError(); + // testInvalidTransactionType(); + // testSuccessfulTransaction(); + // testTransactionNonTecFailure(); + // testTransactionTecFailure(); + // testSuccessfulTransactionMultisigned(); + // testTransactionSigningFailure(); + // testInvalidSingleAndMultiSigningTransaction(); + // testMultisignedBadPubKey(); + // testDeleteExpiredCredentials(); + // testSuccessfulTransactionNetworkID(); + // testSuccessfulPastLedger(); + // testMultipleTransactions(); + testBatchTransaction(); } }; diff --git a/src/xrpld/app/tx/apply.h b/src/xrpld/app/tx/apply.h index 101f9a946d5..409d8737bdb 100644 --- a/src/xrpld/app/tx/apply.h +++ b/src/xrpld/app/tx/apply.h @@ -141,6 +141,14 @@ enum class ApplyTransactionResult { Retry }; +std::optional> +applyBatchTransactions( + Application& app, + OpenView& batchView, + STTx const& batchTxn, + ApplyFlags flags, + beast::Journal j); + /** Transaction application helper Provides more detailed logging and decodes the diff --git a/src/xrpld/app/tx/detail/apply.cpp b/src/xrpld/app/tx/detail/apply.cpp index e2e0adae45c..9c40e9ee405 100644 --- a/src/xrpld/app/tx/detail/apply.cpp +++ b/src/xrpld/app/tx/detail/apply.cpp @@ -171,11 +171,12 @@ apply( }); } -static bool +std::optional> applyBatchTransactions( Application& app, OpenView& batchView, STTx const& batchTxn, + ApplyFlags flags, beast::Journal j) { XRPL_ASSERT( @@ -186,46 +187,70 @@ applyBatchTransactions( auto const parentBatchId = batchTxn.getTransactionID(); auto const mode = batchTxn.getFlags(); - auto applyOneTransaction = - [&app, &j, &parentBatchId, &batchView](STTx&& tx) { - OpenView perTxBatchView(batch_view, batchView); - - auto const ret = - apply(app, perTxBatchView, parentBatchId, tx, tapBATCH, j); + auto applyOneTransaction = [&app, &j, &parentBatchId, &batchView, &flags]( + STTx&& tx) { + OpenView perTxBatchView(batch_view, batchView); + + auto const ret = apply( + app, + perTxBatchView, + parentBatchId, + tx, + (flags & tapDRY_RUN) | tapBATCH, + j); + if (flags & tapDRY_RUN) + { + XRPL_ASSERT( + ret.applied == false, + "Inner transaction should not be applied in dry run"); + } + else + { XRPL_ASSERT( ret.applied == (isTesSuccess(ret.ter) || isTecClaim(ret.ter)), "Inner transaction should not be applied"); + } - JLOG(j.debug()) << "BatchTrace[" << parentBatchId - << "]: " << tx.getTransactionID() << " " - << (ret.applied ? "applied" : "failure") << ": " - << transToken(ret.ter); + JLOG(j.debug()) << "BatchTrace[" << parentBatchId + << "]: " << tx.getTransactionID() << " " + << (ret.applied ? "applied" : "failure") << ": " + << transToken(ret.ter); - // If the transaction should be applied push its changes to the - // whole-batch view. - if (ret.applied && (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) - perTxBatchView.apply(batchView); + // If the transaction should be applied push its changes to the + // whole-batch view. + if ((ret.applied || flags & tapDRY_RUN) && + (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) + perTxBatchView.apply(batchView); - return ret; - }; + return ret; + }; - int applied = 0; + std::vector results; for (STObject rb : batchTxn.getFieldArray(sfRawTransactions)) { auto const result = applyOneTransaction(STTx{std::move(rb)}); - XRPL_ASSERT( - result.applied == - (isTesSuccess(result.ter) || isTecClaim(result.ter)), - "Outer Batch failure, inner transaction should not be applied"); + if (flags & tapDRY_RUN) + { + XRPL_ASSERT( + result.applied == false, + "Inner transaction should not be applied in dry run"); + } + else + { + XRPL_ASSERT( + result.applied == + (isTesSuccess(result.ter) || isTecClaim(result.ter)), + "Outer Batch failure, inner transaction should not be applied"); + } - if (result.applied) - ++applied; + if (result.applied || flags & tapDRY_RUN) + results.push_back(result); if (!isTesSuccess(result.ter)) { if (mode & tfAllOrNothing) - return false; + return std::nullopt; if (mode & tfUntilFailure) break; @@ -234,7 +259,12 @@ applyBatchTransactions( break; } - return applied != 0; + if (results.empty()) + { + return std::nullopt; + } + + return results; } ApplyTransactionResult @@ -268,7 +298,7 @@ applyTransaction( { OpenView wholeBatchView(batch_view, view); - if (applyBatchTransactions(app, wholeBatchView, txn, j)) + if (applyBatchTransactions(app, wholeBatchView, txn, flags, j)) wholeBatchView.apply(view); } diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 830c0cd8f5f..767bb83569f 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -355,6 +355,70 @@ getTxJsonFromParams( return tx_json; } +static Json::Value +processResult( + ApplyResult const& result, + // easier to type as this due to sfRawTransactions in batch transactions + STObject const& transaction, + bool const isBinaryOutput, + LedgerIndex const seq) +{ + Json::Value jvResult = Json::objectValue; + jvResult[jss::applied] = result.applied; + jvResult[jss::ledger_index] = seq; + + // Convert the TER to human-readable values + std::string token; + std::string message; + if (transResultInfo(result.ter, token, message)) + { + // Engine result + jvResult[jss::engine_result] = token; + jvResult[jss::engine_result_code] = result.ter; + jvResult[jss::engine_result_message] = message; + } + else + { + // shouldn't be hit + // LCOV_EXCL_START + jvResult[jss::engine_result] = "unknown"; + jvResult[jss::engine_result_code] = result.ter; + jvResult[jss::engine_result_message] = "unknown"; + // LCOV_EXCL_STOP + } + + if (token == "tesSUCCESS") + { + jvResult[jss::engine_result_message] = + "The simulated transaction would have been applied."; + } + + if (result.metadata) + { + if (isBinaryOutput) + { + auto const metaBlob = + result.metadata->getAsObject().getSerializer().getData(); + jvResult[jss::meta_blob] = strHex(makeSlice(metaBlob)); + } + else + { + jvResult[jss::meta] = result.metadata->getJson(JsonOptions::none); + } + } + + if (isBinaryOutput) + { + auto const txBlob = transaction.getSerializer().getData(); + jvResult[jss::tx_blob] = strHex(makeSlice(txBlob)); + } + else + { + jvResult[jss::tx_json] = transaction.getJson(JsonOptions::none); + } + return jvResult; +} + static Json::Value simulateTxn( RPC::JsonContext& context, @@ -362,15 +426,17 @@ simulateTxn( std::shared_ptr lpLedger, bool const isCurrentLedger) { - Json::Value jvFinalResult; - jvFinalResult[jss::transactions] = Json::arrayValue; + Json::Value jvTransactions = Json::arrayValue; + std::vector results; + bool const isBinaryOutput = context.params.get(jss::binary, false).asBool(); OpenView origView = OpenView(&*lpLedger); OpenView view(batch_view, origView); + LedgerIndex const seq = view.seq(); for (auto const& transaction : transactions) { OpenView perTxView(batch_view, view); - Json::Value jvResult; + auto const txn = *transaction->getSTransaction(); /*************************************** * SECURITY NOTE: This technically applies the transaction to the view. * However, since the `view` is a copy of the ledger it's being applied @@ -379,78 +445,41 @@ simulateTxn( * cannot use `simulate` to bypass signature checks and submit * transactions/modify the current ledger directly. ***************************************/ - auto const result = apply( - context.app, - perTxView, - *transaction->getSTransaction(), - tapDRY_RUN, - context.j); + auto const result = + apply(context.app, perTxView, txn, tapDRY_RUN, context.j); if (isTesSuccess(result.ter) || isTecClaim(result.ter)) perTxView.apply(view); + jvTransactions.append(processResult(result, txn, isBinaryOutput, seq)); - jvResult[jss::applied] = result.applied; - jvResult[jss::ledger_index] = perTxView.seq(); - - bool const isBinaryOutput = - context.params.get(jss::binary, false).asBool(); - - // Convert the TER to human-readable values - std::string token; - std::string message; - if (transResultInfo(result.ter, token, message)) + if (isTesSuccess(result.ter) && txn.getTxnType() == ttBATCH) { - // Engine result - jvResult[jss::engine_result] = token; - jvResult[jss::engine_result_code] = result.ter; - jvResult[jss::engine_result_message] = message; - } - else - { - // shouldn't be hit - // LCOV_EXCL_START - jvResult[jss::engine_result] = "unknown"; - jvResult[jss::engine_result_code] = result.ter; - jvResult[jss::engine_result_message] = "unknown"; - // LCOV_EXCL_STOP - } - - if (token == "tesSUCCESS") - { - jvResult[jss::engine_result_message] = - "The simulated transaction would have been applied."; - } + OpenView wholeBatchView(batch_view, view); - if (result.metadata) - { - if (isBinaryOutput) - { - auto const metaBlob = - result.metadata->getAsObject().getSerializer().getData(); - jvResult[jss::meta_blob] = strHex(makeSlice(metaBlob)); - } - else + if (auto const batchResults = applyBatchTransactions( + context.app, wholeBatchView, txn, tapDRY_RUN, context.j); + batchResults) { - jvResult[jss::meta] = - result.metadata->getJson(JsonOptions::none); + for (int i = 0; i < batchResults->size(); ++i) + { + auto const& innerResult = (*batchResults)[i]; + // TODO: bad, doesn't handle skipping some inner txs + jvTransactions.append(processResult( + innerResult, + txn.getFieldArray(sfRawTransactions)[i], + isBinaryOutput, + seq)); + } + wholeBatchView.apply(view); } } - - if (isBinaryOutput) - { - auto const txBlob = - transaction->getSTransaction()->getSerializer().getData(); - jvResult[jss::tx_blob] = strHex(makeSlice(txBlob)); - } - else - { - jvResult[jss::tx_json] = transaction->getJson(JsonOptions::none); - } - jvFinalResult[jss::transactions].append(jvResult); } - if (jvFinalResult[jss::transactions].size() == 1) + + if (jvTransactions.size() == 1) { - jvFinalResult = jvFinalResult[jss::transactions][0u]; + return jvTransactions[0u]; } + Json::Value jvFinalResult = Json::objectValue; + jvFinalResult[jss::transactions] = jvTransactions; return jvFinalResult; } @@ -507,10 +536,10 @@ processTransaction( return Unexpected(jvResult); } - if (stTx->getTxnType() == ttBATCH) - { - return Unexpected(RPC::make_error(rpcNOT_IMPL)); - } + // if (stTx->getTxnType() == ttBATCH) + // { + // return Unexpected(RPC::make_error(rpcNOT_IMPL)); + // } std::string reason; return std::make_shared(stTx, reason, context.app); From 6506a60c722e2262b2208303c65cd99f078dac88 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 1 Aug 2025 23:05:36 -0400 Subject: [PATCH 14/32] clean up tests --- src/test/rpc/Simulate_test.cpp | 57 ++++++++-------------------------- 1 file changed, 13 insertions(+), 44 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 0e3c1c7702c..2cb5e6736fa 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -597,36 +597,6 @@ class Simulate_test : public beast::unit_test::suite } } - void - testInvalidTransactionType() - { - testcase("Invalid transaction type"); - - using namespace jtx; - - Env env(*this); - - Account const alice{"alice"}; - Account const bob{"bob"}; - env.fund(XRP(1000000), alice, bob); - env.close(); - - auto const batchFee = batch::calcBatchFee(env, 0, 2); - auto const seq = env.seq(alice); - auto jt = env.jtnofill( - batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), - batch::inner(pay(alice, bob, XRP(10)), seq + 1), - batch::inner(pay(alice, bob, XRP(10)), seq + 1)); - - jt.jv.removeMember(jss::TxnSignature); - Json::Value params; - params[jss::tx_json] = jt.jv; - auto const resp = env.rpc("json", "simulate", to_string(params)); - BEAST_EXPECT(resp[jss::result][jss::error] == "notImpl"); - BEAST_EXPECT( - resp[jss::result][jss::error_message] == "Not implemented."); - } - void testSuccessfulTransaction() { @@ -1513,20 +1483,19 @@ class Simulate_test : public beast::unit_test::suite void run() override { - // testParamErrors(); - // testFeeError(); - // testInvalidTransactionType(); - // testSuccessfulTransaction(); - // testTransactionNonTecFailure(); - // testTransactionTecFailure(); - // testSuccessfulTransactionMultisigned(); - // testTransactionSigningFailure(); - // testInvalidSingleAndMultiSigningTransaction(); - // testMultisignedBadPubKey(); - // testDeleteExpiredCredentials(); - // testSuccessfulTransactionNetworkID(); - // testSuccessfulPastLedger(); - // testMultipleTransactions(); + testParamErrors(); + testFeeError(); + testSuccessfulTransaction(); + testTransactionNonTecFailure(); + testTransactionTecFailure(); + testSuccessfulTransactionMultisigned(); + testTransactionSigningFailure(); + testInvalidSingleAndMultiSigningTransaction(); + testMultisignedBadPubKey(); + testDeleteExpiredCredentials(); + testSuccessfulTransactionNetworkID(); + testSuccessfulPastLedger(); + testMultipleTransactions(); testBatchTransaction(); } }; From 3daee179b554fc587b8b83068625f74cbfa28cde Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 4 Aug 2025 13:28:50 -0400 Subject: [PATCH 15/32] fix more tests --- src/test/rpc/LedgerRPC_test.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 1fcec6f22ff..53ba0d00e9d 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -341,15 +341,14 @@ class LedgerRPC_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = "DEADBEEF" + hash3; jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "invalidParams"); - BEAST_EXPECT( - jrr[jss::error_message] == "Invalid field `ledger_hash`."); + BEAST_EXPECT(jrr[jss::error_message] == "ledgerHashMalformed"); // request with non-string ledger_hash jvParams[jss::ledger_hash] = 2; jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::error] == "invalidParams"); BEAST_EXPECT( - jrr[jss::error_message] == "Invalid field `ledger_hash`."); + jrr[jss::error_message] == "Invalid field 'ledger_hash'."); // malformed (non hex) hash jvParams[jss::ledger_hash] = From 0d3e30e697ac9d24fe14d5e4960c231af9012385 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 5 Aug 2025 11:59:11 -0400 Subject: [PATCH 16/32] add more tests --- src/test/rpc/Simulate_test.cpp | 364 ++++++++++++++++++++++++++-- src/xrpld/rpc/CTID.h | 84 +++++-- src/xrpld/rpc/handlers/Simulate.cpp | 110 ++++++--- 3 files changed, 480 insertions(+), 78 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 2cb5e6736fa..26c6e9b8cd8 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -69,8 +69,7 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECT(tx_json[jss::Account] == tx[jss::Account]); BEAST_EXPECT( tx_json[jss::SigningPubKey] == tx.get(jss::SigningPubKey, "")); - BEAST_EXPECT( - tx_json[jss::TxnSignature] == tx.get(jss::TxnSignature, "")); + BEAST_EXPECT(!tx_json.isMember(jss::TxnSignature)); BEAST_EXPECT(tx_json[jss::Fee] == tx.get(jss::Fee, expectedFee)); BEAST_EXPECT( tx_json[jss::Sequence] == tx.get(jss::Sequence, expectedSequence)); @@ -167,11 +166,10 @@ class Simulate_test : public beast::unit_test::suite // No params Json::Value const params = Json::objectValue; auto const resp = env.rpc("json", "simulate", to_string(params)); - BEAST_EXPECTS( + BEAST_EXPECT( resp[jss::result][jss::error_message] == - "None of `tx_blob`, `tx_json`, `tx_hash`, or `ctid` " - "included.", - resp.toStyledString()); + "Must include one of 'transactions', 'tx_json', 'tx_blob', " + "'tx_hash', or 'ctid'."); } { // Providing both `tx_json` and `tx_blob` @@ -182,8 +180,68 @@ class Simulate_test : public beast::unit_test::suite auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( resp[jss::result][jss::error_message] == - "Cannot include 'tx_blob' with 'tx_json', 'tx_hash', or " - "'ctid'."); + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); + } + { + // Providing both `tx_json` and `tx_hash` + Json::Value params = Json::objectValue; + params[jss::tx_json] = Json::objectValue; + params[jss::tx_hash] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); + } + { + // Providing both `tx_json` and `ctid` + Json::Value params = Json::objectValue; + params[jss::tx_json] = Json::objectValue; + params[jss::ctid] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); + } + { + // Providing both `tx_blob` and `tx_hash` + Json::Value params = Json::objectValue; + params[jss::tx_blob] = "1200"; + params[jss::tx_hash] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); + } + { + // Providing both `tx_blob` and `ctid` + Json::Value params = Json::objectValue; + params[jss::tx_blob] = "1200"; + params[jss::ctid] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); + } + { + // Providing both `tx_hash` and `ctid` + Json::Value params = Json::objectValue; + params[jss::tx_hash] = "ABCDEF1234567890"; + params[jss::ctid] = "ABCDEF1234567890"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', 'tx_hash', and 'ctid'."); } { // `binary` isn't a boolean @@ -484,15 +542,14 @@ class Simulate_test : public beast::unit_test::suite "Cannot use `ctid` without `ledger_index` or `ledger_hash`."); } { - // invalid ledger_hash + // invalid tx_hash Json::Value params; params[jss::tx_hash] = "ABCDEF"; params[jss::ledger_index] = 1; auto const resp = env.rpc("json", "simulate", to_string(params)); - BEAST_EXPECTS( + BEAST_EXPECT( resp[jss::result][jss::error_message] == - "Invalid field 'tx_hash'.", - resp.toStyledString()); + "Invalid field 'tx_hash'."); BEAST_EXPECT( resp[jss::result][jss::error_code] == rpcINVALID_PARAMS); } @@ -509,10 +566,22 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECT( resp[jss::result][jss::error_code] == rpcTXN_NOT_FOUND); } + { + // Invalid ctid + Json::Value params; + params[jss::ctid] = "123456"; + params[jss::ledger_index] = 1; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'ctid'."); + BEAST_EXPECT( + resp[jss::result][jss::error_code] == rpcINVALID_PARAMS); + } { // ctid not found Json::Value params; - params[jss::ctid] = "ABCDEF1234567890"; + params[jss::ctid] = "CBADEF1234567890"; params[jss::ledger_index] = 1; auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( @@ -559,6 +628,107 @@ class Simulate_test : public beast::unit_test::suite resp[jss::result][jss::error_message] == "Invalid field 'ledger_index'."); } + { + // Invalid ledger_hash type + Json::Value params; + Json::Value tx_json = Json::objectValue; + tx_json[jss::TransactionType] = jss::AccountSet; + tx_json[jss::Account] = env.master.human(); + params[jss::tx_json] = tx_json; + params[jss::ledger_hash] = Json::arrayValue; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'ledger_hash'."); + } + { + // Invalid transactions array type + Json::Value params; + params[jss::transactions] = "not_an_array"; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'transactions', not array."); + } + { + // Empty transactions array + Json::Value params; + params[jss::transactions] = Json::arrayValue; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'transactions', not nonempty array."); + } + { + // Non-object transaction in transactions array + Json::Value params; + Json::Value txs = Json::arrayValue; + txs.append("not_an_object"); + params[jss::transactions] = txs; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'transactions', not array of objects."); + } + { + // Missing tx_json in transaction object in transactions array + Json::Value params; + Json::Value txs = Json::arrayValue; + Json::Value txObj = Json::objectValue; + txs.append(txObj); + params[jss::transactions] = txs; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Must include one of 'tx_json', 'tx_blob', 'tx_hash', or " + "'ctid' in each transaction."); + } + { + // Non-object tx_json in transaction object in transactions array + Json::Value params; + Json::Value txs = Json::arrayValue; + Json::Value txObj = Json::objectValue; + txObj[jss::tx_json] = "not_an_object"; + txs.append(txObj); + params[jss::transactions] = txs; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECTS( + resp[jss::result][jss::error_message] == + "Invalid field 'tx_json', not object.", + resp.toStyledString()); + } + { + // Multiple fields in transaction object in transactions array + Json::Value params; + Json::Value txs = Json::arrayValue; + Json::Value txObj = Json::objectValue; + txObj[jss::tx_json] = Json::objectValue; + txObj[jss::tx_blob] = "ABCDEF1234567890"; + txs.append(txObj); + params[jss::transactions] = txs; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than one of 'tx_json', 'tx_blob', " + "'tx_hash', and 'ctid' in each transaction."); + } + { + // too long array of transactions + Json::Value params; + Json::Value txs = Json::arrayValue; + for (int i = 0; i < 1001; ++i) + { + Json::Value txObj = Json::objectValue; + txObj[jss::tx_json] = noop(env.master); + txs.append(txObj); + } + params[jss::transactions] = txs; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Cannot include more than 1000 transactions in 'transactions' " + "array."); + } } void @@ -1427,6 +1597,7 @@ class Simulate_test : public beast::unit_test::suite Account const alice{"alice"}; env.fund(XRP(1000), alice); env.close(); + auto const aliceSeq = env.seq(alice); Json::Value tx1; tx1[jss::tx_json] = pay(alice, env.master, XRP(500)); @@ -1435,14 +1606,88 @@ class Simulate_test : public beast::unit_test::suite // TODO: fix sequence autofilling tx2[jss::tx_json][jss::Sequence] = env.seq(alice) + 1; + auto validateOutput = [&](Json::Value const& resp, + Json::Value const& txs) { + auto totalResult = resp[jss::result]; + BEAST_EXPECT(totalResult.isMember(jss::transactions)); + BEAST_EXPECT(totalResult[jss::transactions].size() == 2); + + for (unsigned j = 0; j < 2; ++j) + { + auto const result = totalResult[jss::transactions][j]; + auto const tx = txs[j][jss::tx_json]; + checkBasicReturnValidity( + result, tx, aliceSeq + j, env.current()->fees().base); + BEAST_EXPECTS( + result[jss::engine_result] == "tesSUCCESS", + result.toStyledString()); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT( + result.isMember(jss::meta) || + result.isMember(jss::meta_blob))) + { + Json::Value const metadata = getJsonMetadata(result); + + if (BEAST_EXPECT( + metadata.isMember(sfAffectedNodes.jsonName))) + { + BEAST_EXPECT( + metadata[sfAffectedNodes.jsonName].size() == 2); + auto const masterMode = + metadata[sfAffectedNodes.jsonName][0u]; + if (BEAST_EXPECT( + masterMode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = masterMode[sfModifiedNode]; + BEAST_EXPECT( + modifiedNode[sfLedgerEntryType] == + "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT( + finalFields[sfAccount] == env.master.human()); + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(99999999500) + XRP(200 * j) - + env.current()->fees().base * 2) + .getJson()); + } + auto const aliceNode = + metadata[sfAffectedNodes.jsonName][1u]; + if (BEAST_EXPECT( + aliceNode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = aliceNode[sfModifiedNode]; + BEAST_EXPECT( + modifiedNode[sfLedgerEntryType] == + "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT( + finalFields[sfAccount] == alice.human()); + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(500) - XRP(200 * j) - + env.current()->fees().base * (j + 1)) + .getJson()); + } + } + BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == j); + BEAST_EXPECT( + metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + } + }; + Json::Value params = Json::objectValue; Json::Value txs = Json::arrayValue; txs.append(tx1); txs.append(tx2); params[jss::transactions] = txs; - auto const result = env.rpc("json", "simulate", to_string(params)); - std::cout << result.toStyledString() << std::endl; + validateOutput(env.rpc("json", "simulate", to_string(params)), txs); } void @@ -1475,8 +1720,93 @@ class Simulate_test : public beast::unit_test::suite Json::Value params = Json::objectValue; params[jss::tx_json] = batchTx; - auto const result = env.rpc("json", "simulate", to_string(params)); - std::cout << result.toStyledString() << std::endl; + auto validateOutput = [&](Json::Value const& resp, + Json::Value const& batchTx) { + auto totalResult = resp[jss::result]; + BEAST_EXPECT(totalResult.isMember(jss::transactions)); + BEAST_EXPECT(totalResult[jss::transactions].size() == 3); + + { + auto const outerTx = totalResult[jss::transactions][0u]; + checkBasicReturnValidity(outerTx, batchTx, seq, batchFee); + BEAST_EXPECT(outerTx[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(outerTx[jss::engine_result_code] == 0); + BEAST_EXPECT( + outerTx[jss::engine_result_message] == + "The simulated transaction would have been applied."); + } + + for (unsigned j = 0; j < 2; ++j) + { + auto const result = totalResult[jss::transactions][j + 1u]; + auto const tx = + batchTx[jss::RawTransactions][j][jss::RawTransaction]; + checkBasicReturnValidity(result, tx, seq + j, "0"); + BEAST_EXPECTS( + result[jss::engine_result] == "tesSUCCESS", + result.toStyledString()); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT( + result.isMember(jss::meta) || + result.isMember(jss::meta_blob))) + { + Json::Value const metadata = getJsonMetadata(result); + + if (BEAST_EXPECT( + metadata.isMember(sfAffectedNodes.jsonName))) + { + BEAST_EXPECT( + metadata[sfAffectedNodes.jsonName].size() == 2); + auto const masterMode = + metadata[sfAffectedNodes.jsonName][0u]; + if (BEAST_EXPECT( + masterMode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = masterMode[sfModifiedNode]; + BEAST_EXPECT( + modifiedNode[sfLedgerEntryType] == + "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT( + finalFields[sfAccount] == env.master.human()); + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(99999999500) + XRP(200 * j) - + env.current()->fees().base * 2) + .getJson()); + } + auto const aliceNode = + metadata[sfAffectedNodes.jsonName][1u]; + if (BEAST_EXPECT( + aliceNode.isMember(sfModifiedNode.jsonName))) + { + auto modifiedNode = aliceNode[sfModifiedNode]; + BEAST_EXPECT( + modifiedNode[sfLedgerEntryType] == + "AccountRoot"); + auto finalFields = modifiedNode[sfFinalFields]; + BEAST_EXPECT( + finalFields[sfAccount] == alice.human()); + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(500) - XRP(200 * j) - + env.current()->fees().base * 4) + .getJson()); + } + } + BEAST_EXPECT( + metadata[sfTransactionIndex.jsonName] == j + 1); + BEAST_EXPECT( + metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + } + }; + + validateOutput(env.rpc("json", "simulate", to_string(params)), batchTx); } public: diff --git a/src/xrpld/rpc/CTID.h b/src/xrpld/rpc/CTID.h index be531c536a1..d1c9a206797 100644 --- a/src/xrpld/rpc/CTID.h +++ b/src/xrpld/rpc/CTID.h @@ -39,53 +39,93 @@ namespace RPC { // The Concise Transaction ID provides a way to identify a transaction // that includes which network the transaction was submitted to. +/** + * @brief Encodes ledger sequence, transaction index, and network ID into a CTID + * string. + * + * @param ledgerSeq Ledger sequence number (max 0x0FFF'FFFF). + * @param txnIndex Transaction index within the ledger (max 0xFFFF). + * @param networkID Network identifier (max 0xFFFF). + * @return Optional CTID string in uppercase hexadecimal, or std::nullopt if + * inputs are out of range. + */ inline std::optional encodeCTID(uint32_t ledgerSeq, uint32_t txnIndex, uint32_t networkID) noexcept { - if (ledgerSeq > 0x0FFF'FFFF || txnIndex > 0xFFFF || networkID > 0xFFFF) - return {}; + constexpr uint32_t maxLedgerSeq = 0x0FFF'FFFF; + constexpr uint32_t maxTxnIndex = 0xFFFF; + constexpr uint32_t maxNetworkID = 0xFFFF; + + if (ledgerSeq > maxLedgerSeq || txnIndex > maxTxnIndex || + networkID > maxNetworkID) + return std::nullopt; uint64_t ctidValue = - ((0xC000'0000ULL + static_cast(ledgerSeq)) << 32) + + ((0xC000'0000ULL + static_cast(ledgerSeq)) << 32) | (static_cast(txnIndex) << 16) + networkID; std::stringstream buffer; buffer << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << ctidValue; - return {buffer.str()}; + return buffer.str(); } +/** + * @brief Decodes a CTID string or integer into its component parts. + * + * @tparam T Type of the CTID input (string, string_view, char*, integral). + * @param ctid CTID value to decode. + * @return Optional tuple of (ledgerSeq, txnIndex, networkID), or std::nullopt + * if invalid. + */ template inline std::optional> decodeCTID(T const ctid) noexcept { - uint64_t ctidValue{0}; + uint64_t ctidValue = 0; + if constexpr ( - std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v) + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v) { std::string const ctidString(ctid); - if (ctidString.length() != 16) - return {}; - - if (!boost::regex_match(ctidString, boost::regex("^[0-9A-Fa-f]+$"))) - return {}; - - ctidValue = std::stoull(ctidString, nullptr, 16); + if (ctidString.size() != 16) + return std::nullopt; + + static boost::regex const hexRegex("^[0-9A-Fa-f]{16}$"); + if (!boost::regex_match(ctidString, hexRegex)) + return std::nullopt; + + try + { + ctidValue = std::stoull(ctidString, nullptr, 16); + } + catch (...) + { + return std::nullopt; + } } else if constexpr (std::is_integral_v) - ctidValue = ctid; + { + ctidValue = static_cast(ctid); + } else - return {}; + { + return std::nullopt; + } + + // Validate CTID prefix. + constexpr uint64_t ctidPrefixMask = 0xF000'0000'0000'0000ULL; + constexpr uint64_t ctidPrefix = 0xC000'0000'0000'0000ULL; + if ((ctidValue & ctidPrefixMask) != ctidPrefix) + return std::nullopt; - if ((ctidValue & 0xF000'0000'0000'0000ULL) != 0xC000'0000'0000'0000ULL) - return {}; + uint32_t ledgerSeq = static_cast((ctidValue >> 32) & 0x0FFF'FFFF); + uint16_t txnIndex = static_cast((ctidValue >> 16) & 0xFFFF); + uint16_t networkID = static_cast(ctidValue & 0xFFFF); - uint32_t ledger_seq = (ctidValue >> 32) & 0xFFFF'FFFUL; - uint16_t txn_index = (ctidValue >> 16) & 0xFFFFU; - uint16_t network_id = ctidValue & 0xFFFFU; - return {{ledger_seq, txn_index, network_id}}; + return std::make_tuple(ledgerSeq, txnIndex, networkID); } } // namespace RPC diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 767bb83569f..124c3d4ed8e 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -36,6 +36,8 @@ namespace ripple { +constexpr int const MAX_SIMULATE_TXS = 1000; + static Expected getAutofillSequence( Json::Value const& tx_json, @@ -149,15 +151,17 @@ autofillTx( } } - if (!tx_json.isMember(jss::TxnSignature)) - { - // autofill TxnSignature - tx_json[jss::TxnSignature] = ""; - } - else if (tx_json[jss::TxnSignature] != "") + if (tx_json.isMember(jss::TxnSignature)) { - // Transaction must not be signed - return rpcError(rpcTX_SIGNED); + if (tx_json[jss::TxnSignature] != "") + { + // Transaction must not be signed + return rpcError(rpcTX_SIGNED); + } + else + { + tx_json.removeMember(jss::TxnSignature); + } } if (!tx_json.isMember(jss::Sequence)) @@ -186,13 +190,6 @@ getTxJsonFromHistory(RPC::JsonContext& context, bool const isCurrentLedger) uint256 hash; if (params.isMember(jss::tx_hash)) { - if (params.isMember(jss::tx_blob) || params.isMember(jss::tx_json) || - params.isMember(jss::ctid)) - { - return RPC::make_param_error( - "Cannot include 'tx_hash' with 'ctid'."); - } - auto const tx_hash = params[jss::tx_hash]; if (!tx_hash.isString()) { @@ -221,13 +218,13 @@ getTxJsonFromHistory(RPC::JsonContext& context, bool const isCurrentLedger) } auto decodedCTID = RPC::decodeCTID(context.params[jss::ctid].asString()); - auto const [lgr_seq, txn_idx, net_id] = *decodedCTID; - if (!ctid) + if (!decodedCTID) { return RPC::invalid_field_error(jss::ctid); } + auto const [ledgerSq, txId, _] = *decodedCTID; if (auto const optHash = - context.app.getLedgerMaster().txnIdFromIndex(lgr_seq, txn_idx); + context.app.getLedgerMaster().txnIdFromIndex(ledgerSq, txId); optHash) { hash = *optHash; @@ -285,14 +282,6 @@ getTxJsonFromParams( if (txInput.isMember(jss::tx_blob)) { - if (txInput.isMember(jss::tx_json) || txInput.isMember(jss::tx_hash) || - txInput.isMember(jss::ctid)) - { - return RPC::make_param_error( - "Cannot include 'tx_blob' with 'tx_json', 'tx_hash', or " - "'ctid'."); - } - auto const tx_blob = txInput[jss::tx_blob]; if (!tx_blob.isString()) { @@ -316,12 +305,6 @@ getTxJsonFromParams( } else if (txInput.isMember(jss::tx_json)) { - if (txInput.isMember(jss::tx_hash) || txInput.isMember(jss::ctid)) - { - return RPC::make_param_error( - "Cannot include 'tx_json' with 'tx_hash' or 'ctid'."); - } - tx_json = txInput[jss::tx_json]; if (!tx_json.isObject()) { @@ -573,16 +556,26 @@ doSimulate(RPC::JsonContext& context) } } - if (context.params.isMember(jss::transactions) && - (context.params.isMember(jss::tx_json) || - context.params.isMember(jss::tx_blob) || - context.params.isMember(jss::tx_hash) || - context.params.isMember(jss::ctid))) + auto const numParams = + (context.params.isMember(jss::transactions) + + context.params.isMember(jss::tx_json) + + context.params.isMember(jss::tx_blob) + + context.params.isMember(jss::tx_hash) + + context.params.isMember(jss::ctid)); + if (numParams == 0) { return RPC::make_param_error( - "Cannot include 'transactions' with 'tx_json', 'tx_blob', " + "Must include one of 'transactions', 'tx_json', 'tx_blob', " "'tx_hash', or 'ctid'."); } + // if more than one of these fields is included, error out + if (numParams > 1) + { + return RPC::make_param_error( + "Cannot include more than one of 'transactions', 'tx_json', " + "'tx_blob', " + "'tx_hash', and 'ctid'."); + } std::shared_ptr lpLedger; auto jvResult = RPC::lookupLedger(lpLedger, context); @@ -593,9 +586,48 @@ doSimulate(RPC::JsonContext& context) auto transactions = std::vector>{}; if (context.params.isMember(jss::transactions)) { - // TODO: bunch of additional checks + auto const txs = context.params[jss::transactions]; + if (!txs.isArray()) + { + return RPC::expected_field_error(jss::transactions, "array"); + } + if (txs.size() == 0) + { + return RPC::expected_field_error( + jss::transactions, "nonempty array"); + } + if (txs.size() > MAX_SIMULATE_TXS) + { + return RPC::make_param_error( + "Cannot include more than " + std::to_string(MAX_SIMULATE_TXS) + + " transactions in 'transactions' array."); + } for (auto const& txInput : context.params[jss::transactions]) { + if (!txInput.isObject()) + { + return RPC::expected_field_error( + jss::transactions, "array of objects"); + } + { + auto const numParams = txInput.isMember(jss::tx_json) + + txInput.isMember(jss::tx_blob) + + txInput.isMember(jss::tx_hash) + + txInput.isMember(jss::ctid); + if (numParams == 0) + { + return RPC::make_param_error( + "Must include one of 'tx_json', 'tx_blob', " + "'tx_hash', or 'ctid' in each transaction."); + } + else if (numParams > 1) + { + return RPC::make_param_error( + "Cannot include more than one of 'tx_json', 'tx_blob', " + "'tx_hash', and 'ctid' in each transaction."); + } + } + auto transaction = processTransaction(context, txInput, lpLedger, isCurrentLedger); if (!transaction) From 2c94917286030ed7deadb8868681713b3b96ee08 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 5 Aug 2025 17:52:58 -0400 Subject: [PATCH 17/32] fix gcc build issue --- src/xrpld/rpc/CTID.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/rpc/CTID.h b/src/xrpld/rpc/CTID.h index d1c9a206797..47e67a4e8cf 100644 --- a/src/xrpld/rpc/CTID.h +++ b/src/xrpld/rpc/CTID.h @@ -62,7 +62,7 @@ encodeCTID(uint32_t ledgerSeq, uint32_t txnIndex, uint32_t networkID) noexcept uint64_t ctidValue = ((0xC000'0000ULL + static_cast(ledgerSeq)) << 32) | - (static_cast(txnIndex) << 16) + networkID; + ((static_cast(txnIndex) << 16) | networkID); std::stringstream buffer; buffer << std::hex << std::uppercase << std::setfill('0') << std::setw(16) From 6c05d8eac7e7486240a1da2ab4dc4053634ddcd5 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 13 Aug 2025 17:16:42 -0400 Subject: [PATCH 18/32] do some cleanup --- src/xrpld/rpc/handlers/Simulate.cpp | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 124c3d4ed8e..e9450c10e9e 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -445,7 +445,6 @@ simulateTxn( for (int i = 0; i < batchResults->size(); ++i) { auto const& innerResult = (*batchResults)[i]; - // TODO: bad, doesn't handle skipping some inner txs jvTransactions.append(processResult( innerResult, txn.getFieldArray(sfRawTransactions)[i], @@ -519,11 +518,6 @@ processTransaction( return Unexpected(jvResult); } - // if (stTx->getTxnType() == ttBATCH) - // { - // return Unexpected(RPC::make_error(rpcNOT_IMPL)); - // } - std::string reason; return std::make_shared(stTx, reason, context.app); } @@ -531,8 +525,8 @@ processTransaction( // { // tx_blob: XOR tx_json: XOR tx_hash: XOR ctid: // XOR -// transactions: [ ] // array of objects with one of -// tx_json/tx_blob/tx_hash/ctid, +// transactions: [ ] +// (each one of tx_json/tx_blob/tx_hash/ctid), // binary: // } Json::Value From 2088fcf09c5166171969c105356174b6983a8334 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 13 Aug 2025 17:23:27 -0400 Subject: [PATCH 19/32] more cleanup --- src/xrpld/rpc/handlers/Simulate.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index e9450c10e9e..9a5cf0796a0 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -234,11 +234,6 @@ getTxJsonFromHistory(RPC::JsonContext& context, bool const isCurrentLedger) return RPC::make_error(rpcTXN_NOT_FOUND); } } - if (!hash) - { - return RPC::make_param_error( - "None of `tx_blob`, `tx_json`, `tx_hash`, or `ctid` included."); - } using TxPair = std::pair, std::shared_ptr>; auto ec{rpcSUCCESS}; From 426fa701c2b15f6f6b35cd0aa304d89d48638d53 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 24 Sep 2025 20:33:17 -0400 Subject: [PATCH 20/32] builds but fails tests --- src/xrpld/rpc/handlers/Simulate.cpp | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 4264e1448f7..eef1dda36de 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -385,17 +385,18 @@ processResult( else { jvResult[jss::meta] = result.metadata->getJson(JsonOptions::none); - RPC::insertDeliveredAmount( - jvResult[jss::meta], - view, - transaction->getSTransaction(), - *result.metadata); - RPC::insertNFTSyntheticInJson( - jvResult, transaction->getSTransaction(), *result.metadata); - RPC::insertMPTokenIssuanceID( - jvResult[jss::meta], - transaction->getSTransaction(), - *result.metadata); + // TODO: fix + // RPC::insertDeliveredAmount( + // jvResult[jss::meta], + // view, + // transaction->getSTransaction(), + // *result.metadata); + // RPC::insertNFTSyntheticInJson( + // jvResult, transaction->getSTransaction(), *result.metadata); + // RPC::insertMPTokenIssuanceID( + // jvResult[jss::meta], + // transaction->getSTransaction(), + // *result.metadata); } } From 4f6c2aa56d85a179a25f82b76abe2eb1a1a6c6d2 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 13 Oct 2025 17:06:18 -0400 Subject: [PATCH 21/32] fix synthetic fields --- src/xrpld/rpc/handlers/Simulate.cpp | 36 +++++++++++++---------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index eef1dda36de..5448b7b1cb6 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -340,13 +340,13 @@ static Json::Value processResult( ApplyResult const& result, // easier to type as this due to sfRawTransactions in batch transactions - STObject const& transaction, + STTx const& transaction, bool const isBinaryOutput, - LedgerIndex const seq) + ReadView const& view) { Json::Value jvResult = Json::objectValue; jvResult[jss::applied] = result.applied; - jvResult[jss::ledger_index] = seq; + jvResult[jss::ledger_index] = view.seq(); // Convert the TER to human-readable values std::string token; @@ -385,18 +385,12 @@ processResult( else { jvResult[jss::meta] = result.metadata->getJson(JsonOptions::none); - // TODO: fix - // RPC::insertDeliveredAmount( - // jvResult[jss::meta], - // view, - // transaction->getSTransaction(), - // *result.metadata); - // RPC::insertNFTSyntheticInJson( - // jvResult, transaction->getSTransaction(), *result.metadata); - // RPC::insertMPTokenIssuanceID( - // jvResult[jss::meta], - // transaction->getSTransaction(), - // *result.metadata); + auto const shared = std::make_shared(transaction); + RPC::insertDeliveredAmount( + jvResult[jss::meta], view, shared, *result.metadata); + RPC::insertNFTSyntheticInJson(jvResult, shared, *result.metadata); + RPC::insertMPTokenIssuanceID( + jvResult[jss::meta], shared, *result.metadata); } } @@ -442,7 +436,7 @@ simulateTxn( apply(context.app, perTxView, txn, tapDRY_RUN, context.j); if (isTesSuccess(result.ter) || isTecClaim(result.ter)) perTxView.apply(view); - jvTransactions.append(processResult(result, txn, isBinaryOutput, seq)); + jvTransactions.append(processResult(result, txn, isBinaryOutput, view)); if (isTesSuccess(result.ter) && txn.getTxnType() == ttBATCH) { @@ -455,11 +449,13 @@ simulateTxn( for (int i = 0; i < batchResults->size(); ++i) { auto const& innerResult = (*batchResults)[i]; + auto rawTxn = txn.getFieldArray(sfRawTransactions)[i]; + STTx const txn( + static_cast( + rawTxn.getFieldU16(sfTransactionType)), + [&rawTxn](STObject& obj) { obj = STObject(rawTxn); }); jvTransactions.append(processResult( - innerResult, - txn.getFieldArray(sfRawTransactions)[i], - isBinaryOutput, - seq)); + innerResult, txn, isBinaryOutput, wholeBatchView)); } wholeBatchView.apply(view); } From 0cbe4e6daec56d983597fb33d12b80b1671f0d27 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 13 Oct 2025 17:19:49 -0400 Subject: [PATCH 22/32] remove unused variable --- src/xrpld/rpc/handlers/Simulate.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 5448b7b1cb6..416268165d6 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -419,7 +419,6 @@ simulateTxn( OpenView origView = OpenView(&*lpLedger); OpenView view(batch_view, origView); - LedgerIndex const seq = view.seq(); for (auto const& transaction : transactions) { OpenView perTxView(batch_view, view); From be70f58faef678e5b45ac0cd246c0bb1cd4bb191 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 13 Oct 2025 17:59:47 -0400 Subject: [PATCH 23/32] fix ledger_entry tests --- src/test/rpc/LedgerEntry_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index a88f6ab612a..cd03191566b 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -498,7 +498,7 @@ class LedgerEntry_test : public beast::unit_test::suite to_string(jvParams))[jss::result]; auto const expectedErrMsg = fieldValue.isString() ? "ledgerHashMalformed" - : "ledgerHashNotString"; + : "Invalid field 'ledger_hash'."; checkErrorValue(jrr, "invalidParams", expectedErrMsg); }; From 00bd84dd9955d1675baf432b6785732392a13cb7 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 10 Dec 2025 11:15:08 -0800 Subject: [PATCH 24/32] fix build + test issues --- src/test/rpc/Simulate_test.cpp | 4 ++-- src/xrpld/rpc/handlers/Simulate.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index b7e0d435e75..5f345ab3252 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -639,7 +639,7 @@ class Simulate_test : public beast::unit_test::suite auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( resp[jss::result][jss::error_message] == - "Invalid field 'ledger_index'."); + "Invalid field 'ledger_index', not string or number."); } { // Invalid ledger_hash type @@ -652,7 +652,7 @@ class Simulate_test : public beast::unit_test::suite auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( resp[jss::result][jss::error_message] == - "Invalid field 'ledger_hash'."); + "Invalid field 'ledger_hash', not hex string."); } { // Invalid transactions array type diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 27c334018d1..590bb6ee5c5 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include @@ -468,7 +468,7 @@ checkIsCurrentLedger(Json::Value const params) auto const& ledgerIndex = params[jss::ledger_index]; if (!ledgerIndex.isNull()) { - return ledgerIndex == RPC::LedgerShortcut::CURRENT; + return ledgerIndex == jss::current; } } if (params.isMember(jss::ledger_hash)) From b05d921ceba963e1ea836b474d243323e33cf74b Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 23 Dec 2025 22:05:42 -0800 Subject: [PATCH 25/32] fix merge issue --- src/test/rpc/Simulate_test.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 249c666125b..17a7098b0e2 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -1580,14 +1580,15 @@ class Simulate_test : public beast::unit_test::suite env.fund(XRP(1000), alice); env.close(); - auto const ledgerSeq = env.current()->info().seq; + auto const ledgerSeq = env.current()->header().seq; auto const aliceSeq = env.seq(alice); env.close(); Json::Value tx = pay(alice, env.master, XRP(700)); env(tx); auto const txHash = to_string(env.tx()->getTransactionID()); - auto const ctid = *RPC::encodeCTID(env.current()->info().seq, 0, netID); + auto const ctid = + *RPC::encodeCTID(env.current()->header().seq, 0, netID); env.close(); { From 3f75ab086114390a19cac3768739543b209988de Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 13 Feb 2026 17:52:01 -0500 Subject: [PATCH 26/32] fix build issue --- src/xrpld/rpc/handlers/Simulate.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index bccc568cce0..3a2bdc63c61 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include From 44a947e07bc1f627b2435d67ebd3b00d0a33bb3d Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 24 Feb 2026 17:22:01 -0500 Subject: [PATCH 27/32] fix formatting --- src/test/rpc/Simulate_test.cpp | 73 ++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 5918fbcadcb..fa111179bb4 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -500,7 +500,8 @@ class Simulate_test : public beast::unit_test::suite params[jss::ctid] = "ABCDEF1234567890"; auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( - resp[jss::result][jss::error_message] == "Cannot use `ctid` without `ledger_index` or `ledger_hash`."); + resp[jss::result][jss::error_message] == + "Cannot use `ctid` without `ledger_index` or `ledger_hash`."); } { // invalid tx_hash @@ -517,7 +518,9 @@ class Simulate_test : public beast::unit_test::suite params[jss::tx_hash] = std::string(64, 'A'); params[jss::ledger_index] = 1; auto const resp = env.rpc("json", "simulate", to_string(params)); - BEAST_EXPECTS(resp[jss::result][jss::error_message] == "Transaction not found.", resp.toStyledString()); + BEAST_EXPECTS( + resp[jss::result][jss::error_message] == "Transaction not found.", + resp.toStyledString()); BEAST_EXPECT(resp[jss::result][jss::error_code] == rpcTXN_NOT_FOUND); } { @@ -547,7 +550,9 @@ class Simulate_test : public beast::unit_test::suite tx_json["UnknownField"] = "value"; params[jss::tx_json] = tx_json; auto const resp = env.rpc("json", "simulate", to_string(params)); - BEAST_EXPECT(resp[jss::result][jss::error_message] == "Field 'tx_json.UnknownField' is unknown."); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Field 'tx_json.UnknownField' is unknown."); } { // TicketSequence autofill returns 0 @@ -570,7 +575,8 @@ class Simulate_test : public beast::unit_test::suite params[jss::ledger_index] = Json::arrayValue; auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( - resp[jss::result][jss::error_message] == "Invalid field 'ledger_index', not string or number."); + resp[jss::result][jss::error_message] == + "Invalid field 'ledger_index', not string or number."); } { // Invalid ledger_hash type @@ -581,21 +587,27 @@ class Simulate_test : public beast::unit_test::suite params[jss::tx_json] = tx_json; params[jss::ledger_hash] = Json::arrayValue; auto const resp = env.rpc("json", "simulate", to_string(params)); - BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'ledger_hash', not hex string."); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'ledger_hash', not hex string."); } { // Invalid transactions array type Json::Value params; params[jss::transactions] = "not_an_array"; auto const resp = env.rpc("json", "simulate", to_string(params)); - BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'transactions', not array."); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'transactions', not array."); } { // Empty transactions array Json::Value params; params[jss::transactions] = Json::arrayValue; auto const resp = env.rpc("json", "simulate", to_string(params)); - BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'transactions', not nonempty array."); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'transactions', not nonempty array."); } { // Non-object transaction in transactions array @@ -605,7 +617,8 @@ class Simulate_test : public beast::unit_test::suite params[jss::transactions] = txs; auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( - resp[jss::result][jss::error_message] == "Invalid field 'transactions', not array of objects."); + resp[jss::result][jss::error_message] == + "Invalid field 'transactions', not array of objects."); } { // Missing tx_json in transaction object in transactions array @@ -630,7 +643,8 @@ class Simulate_test : public beast::unit_test::suite params[jss::transactions] = txs; auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECTS( - resp[jss::result][jss::error_message] == "Invalid field 'tx_json', not object.", resp.toStyledString()); + resp[jss::result][jss::error_message] == "Invalid field 'tx_json', not object.", + resp.toStyledString()); } { // Multiple fields in transaction object in transactions array @@ -1412,7 +1426,9 @@ class Simulate_test : public beast::unit_test::suite Json::Value request = Json::objectValue; request[jss::tx_json] = tx; auto const result = env.rpc("json", "simulate", to_string(request)); - BEAST_EXPECTS(result[jss::result][jss::engine_result] == "tecUNFUNDED_PAYMENT", result.toStyledString()); + BEAST_EXPECTS( + result[jss::result][jss::engine_result] == "tecUNFUNDED_PAYMENT", + result.toStyledString()); } { @@ -1423,7 +1439,8 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECTS(result[jss::engine_result] == "tesSUCCESS", result.toStyledString()); BEAST_EXPECT(result[jss::engine_result_code] == 0); BEAST_EXPECT( - result[jss::engine_result_message] == "The simulated transaction would have been applied."); + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) { @@ -1439,7 +1456,8 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); auto finalFields = modifiedNode[sfFinalFields]; BEAST_EXPECTS( - finalFields[sfBalance] == (XRP(99999999700) - env.current()->fees().base * 2).getJson(), + finalFields[sfBalance] == + (XRP(99999999700) - env.current()->fees().base * 2).getJson(), metadata.toStyledString()); } auto const aliceNode = metadata[sfAffectedNodes.jsonName][1u]; @@ -1448,7 +1466,9 @@ class Simulate_test : public beast::unit_test::suite auto modifiedNode = aliceNode[sfModifiedNode]; BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot"); auto finalFields = modifiedNode[sfFinalFields]; - BEAST_EXPECT(finalFields[sfBalance] == (XRP(300) - env.current()->fees().base).getJson()); + BEAST_EXPECT( + finalFields[sfBalance] == + (XRP(300) - env.current()->fees().base).getJson()); } } BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); @@ -1517,7 +1537,8 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECTS(result[jss::engine_result] == "tesSUCCESS", result.toStyledString()); BEAST_EXPECT(result[jss::engine_result_code] == 0); BEAST_EXPECT( - result[jss::engine_result_message] == "The simulated transaction would have been applied."); + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) { @@ -1535,7 +1556,8 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECT(finalFields[sfAccount] == env.master.human()); BEAST_EXPECT( finalFields[sfBalance] == - (XRP(99999999500) + XRP(200 * j) - env.current()->fees().base * 2).getJson()); + (XRP(99999999500) + XRP(200 * j) - env.current()->fees().base * 2) + .getJson()); } auto const aliceNode = metadata[sfAffectedNodes.jsonName][1u]; if (BEAST_EXPECT(aliceNode.isMember(sfModifiedNode.jsonName))) @@ -1546,7 +1568,8 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECT(finalFields[sfAccount] == alice.human()); BEAST_EXPECT( finalFields[sfBalance] == - (XRP(500) - XRP(200 * j) - env.current()->fees().base * (j + 1)).getJson()); + (XRP(500) - XRP(200 * j) - env.current()->fees().base * (j + 1)) + .getJson()); } } BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == j); @@ -1580,9 +1603,11 @@ class Simulate_test : public beast::unit_test::suite auto const batchFee = batch::calcBatchFee(env, 0, 2); Json::Value batchTx = batch::outer(alice, seq, batchFee, tfIndependent); Json::Value tx1 = Json::objectValue; - tx1[jss::RawTransaction] = batch::inner(pay(alice, env.master, XRP(500)), env.seq(alice) + 1).getTxn(); + tx1[jss::RawTransaction] = + batch::inner(pay(alice, env.master, XRP(500)), env.seq(alice) + 1).getTxn(); Json::Value tx2 = Json::objectValue; - tx2[jss::RawTransaction] = batch::inner(pay(alice, env.master, XRP(200)), env.seq(alice) + 2).getTxn(); + tx2[jss::RawTransaction] = + batch::inner(pay(alice, env.master, XRP(200)), env.seq(alice) + 2).getTxn(); batchTx[jss::RawTransactions] = Json::arrayValue; batchTx[jss::RawTransactions].append(tx1); batchTx[jss::RawTransactions].append(tx2); @@ -1601,7 +1626,8 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECT(outerTx[jss::engine_result] == "tesSUCCESS"); BEAST_EXPECT(outerTx[jss::engine_result_code] == 0); BEAST_EXPECT( - outerTx[jss::engine_result_message] == "The simulated transaction would have been applied."); + outerTx[jss::engine_result_message] == + "The simulated transaction would have been applied."); } for (unsigned j = 0; j < 2; ++j) @@ -1612,7 +1638,8 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECTS(result[jss::engine_result] == "tesSUCCESS", result.toStyledString()); BEAST_EXPECT(result[jss::engine_result_code] == 0); BEAST_EXPECT( - result[jss::engine_result_message] == "The simulated transaction would have been applied."); + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) { @@ -1630,7 +1657,8 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECT(finalFields[sfAccount] == env.master.human()); BEAST_EXPECT( finalFields[sfBalance] == - (XRP(99999999500) + XRP(200 * j) - env.current()->fees().base * 2).getJson()); + (XRP(99999999500) + XRP(200 * j) - env.current()->fees().base * 2) + .getJson()); } auto const aliceNode = metadata[sfAffectedNodes.jsonName][1u]; if (BEAST_EXPECT(aliceNode.isMember(sfModifiedNode.jsonName))) @@ -1641,7 +1669,8 @@ class Simulate_test : public beast::unit_test::suite BEAST_EXPECT(finalFields[sfAccount] == alice.human()); BEAST_EXPECT( finalFields[sfBalance] == - (XRP(500) - XRP(200 * j) - env.current()->fees().base * 4).getJson()); + (XRP(500) - XRP(200 * j) - env.current()->fees().base * 4) + .getJson()); } } BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == j + 1); From 406e18f0484830a99c40aa749ad31b2f74be2f51 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 24 Feb 2026 17:47:40 -0500 Subject: [PATCH 28/32] fix --- include/xrpl/tx/apply.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xrpl/tx/apply.h b/include/xrpl/tx/apply.h index 242c508e7b8..a920e6f779a 100644 --- a/include/xrpl/tx/apply.h +++ b/include/xrpl/tx/apply.h @@ -117,7 +117,7 @@ enum class ApplyTransactionResult { std::optional> applyBatchTransactions( - Application& app, + Registry& registry, OpenView& batchView, STTx const& batchTxn, ApplyFlags flags, From 876bed890bc3c0989fc6d229bbb6c0d1fa8cf694 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 26 Feb 2026 12:15:10 -0500 Subject: [PATCH 29/32] fix build issue --- include/xrpl/tx/apply.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/xrpl/tx/apply.h b/include/xrpl/tx/apply.h index a920e6f779a..204d7a77b2c 100644 --- a/include/xrpl/tx/apply.h +++ b/include/xrpl/tx/apply.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include From 8f4a532ad25e38cad2eb8f0b07446720d8ed5fab Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 26 Feb 2026 12:24:39 -0500 Subject: [PATCH 30/32] oops --- include/xrpl/tx/apply.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xrpl/tx/apply.h b/include/xrpl/tx/apply.h index 204d7a77b2c..f844af9a4f6 100644 --- a/include/xrpl/tx/apply.h +++ b/include/xrpl/tx/apply.h @@ -118,7 +118,7 @@ enum class ApplyTransactionResult { std::optional> applyBatchTransactions( - Registry& registry, + ServiceRegistry& registry, OpenView& batchView, STTx const& batchTxn, ApplyFlags flags, From 55b8469e15467fed0ab4b4ebb21319352bf8d0c8 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 26 Feb 2026 14:59:34 -0500 Subject: [PATCH 31/32] fix import --- src/xrpld/rpc/handlers/Simulate.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 9d41e7d52f9..bd90b49e2bd 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include From e0316e6b89593feb32f8193ed06ff416521826f9 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 26 Feb 2026 15:48:58 -0500 Subject: [PATCH 32/32] fix build --- src/xrpld/rpc/handlers/Simulate.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index bd90b49e2bd..8dc32762e81 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -54,7 +54,7 @@ getAutofillSequence( } if (!sle) { - JLOG(context.registry.journal("Simulate").debug()) + JLOG(context.app.journal("Simulate").debug()) << "Failed to find source account " << "in current ledger: " << toBase58(*srcAddressID); @@ -63,7 +63,7 @@ getAutofillSequence( if (!isCurrentLedger) return sle->getFieldU32(sfSequence); - return context.registry.getTxQ().nextQueuableSeq(sle).value(); + return context.app.getTxQ().nextQueuableSeq(sle).value(); } static std::optional @@ -137,10 +137,10 @@ autofillTx( { auto feeOrError = RPC::getCurrentNetworkFee( context.role, - context.registry.config(), - context.registry.getFeeTrack(), - context.registry.getTxQ(), - context.registry, + context.app.config(), + context.app.getFeeTrack(), + context.app.getTxQ(), + context.app, tx_json); if (feeOrError.isMember(jss::error)) return feeOrError; @@ -166,7 +166,7 @@ autofillTx( if (!tx_json.isMember(jss::NetworkID)) { - auto const networkId = context.registry.getNetworkIDService().getNetworkID(); + auto const networkId = context.app.getNetworkIDService().getNetworkID(); if (networkId > 1024) tx_json[jss::NetworkID] = to_string(networkId); } @@ -213,7 +213,7 @@ getTxJsonFromHistory(RPC::JsonContext& context, bool const isCurrentLedger) return RPC::invalid_field_error(jss::ctid); } auto const [ledgerSq, txId, _] = *decodedCTID; - if (auto const optHash = context.registry.getLedgerMaster().txnIdFromIndex(ledgerSq, txId); + if (auto const optHash = context.app.getLedgerMaster().txnIdFromIndex(ledgerSq, txId); optHash) { hash = *optHash; @@ -225,7 +225,7 @@ getTxJsonFromHistory(RPC::JsonContext& context, bool const isCurrentLedger) } using TxPair = std::pair, std::shared_ptr>; auto ec{rpcSUCCESS}; - std::variant v = context.registry.getMasterTransaction().fetch(hash, ec); + std::variant v = context.app.getMasterTransaction().fetch(hash, ec); if (std::get_if(&v)) { return RPC::make_error(rpcTXN_NOT_FOUND); @@ -407,7 +407,7 @@ simulateTxn( * cannot use `simulate` to bypass signature checks and submit * transactions/modify the current ledger directly. ***************************************/ - auto const result = apply(context.registry, perTxView, txn, tapDRY_RUN, context.j); + auto const result = apply(context.app, perTxView, txn, tapDRY_RUN, context.j); if (isTesSuccess(result.ter) || isTecClaim(result.ter)) perTxView.apply(view); jvTransactions.append(processResult(result, txn, isBinaryOutput, view)); @@ -416,8 +416,8 @@ simulateTxn( { OpenView wholeBatchView(batch_view, view); - if (auto const batchResults = applyBatchTransactions( - context.registry, wholeBatchView, txn, tapDRY_RUN, context.j); + if (auto const batchResults = + applyBatchTransactions(context.app, wholeBatchView, txn, tapDRY_RUN, context.j); batchResults) { for (int i = 0; i < batchResults->size(); ++i) @@ -497,7 +497,7 @@ processTransaction( } std::string reason; - return std::make_shared(stTx, reason, context.registry); + return std::make_shared(stTx, reason, context.app); } // {