Files
rippled/src/test/rpc/Simulate_test.cpp
2026-04-07 14:47:47 +02:00

1187 lines
45 KiB
C++

#include <test/jtx.h>
#include <test/jtx/Env.h>
#include <test/jtx/envconfig.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/rpc/CTID.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/serialize.h>
#include <optional>
#include <tuple>
namespace xrpl {
namespace test {
class Simulate_test : public beast::unit_test::suite
{
void
checkBasicReturnValidity(
Json::Value const& result,
Json::Value const& tx,
int const expectedSequence,
std::string const& expectedFee)
{
BEAST_EXPECT(result[jss::applied] == false);
BEAST_EXPECT(result.isMember(jss::engine_result));
BEAST_EXPECT(result.isMember(jss::engine_result_code));
BEAST_EXPECT(result.isMember(jss::engine_result_message));
BEAST_EXPECT(result.isMember(jss::tx_json) || result.isMember(jss::tx_blob));
Json::Value tx_json;
if (result.isMember(jss::tx_json))
{
tx_json = result[jss::tx_json];
}
else
{
auto const unHexed = strUnHex(result[jss::tx_blob].asString());
SerialIter sitTrans(makeSlice(*unHexed)); // NOLINT(bugprone-unchecked-optional-access)
tx_json = STObject(std::ref(sitTrans), sfGeneric).getJson(JsonOptions::none);
}
BEAST_EXPECT(tx_json[jss::TransactionType] == tx[jss::TransactionType]);
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[jss::Fee] == tx.get(jss::Fee, expectedFee));
BEAST_EXPECT(tx_json[jss::Sequence] == tx.get(jss::Sequence, expectedSequence));
}
void
checkBasicReturnValidity(
Json::Value const& result,
Json::Value const& tx,
int const expectedSequence,
XRPAmount const& expectedFee)
{
checkBasicReturnValidity(
result, tx, expectedSequence, expectedFee.jsonClipped().asString());
}
void
testTx(
jtx::Env& env,
Json::Value const& tx,
std::function<void(Json::Value const&, Json::Value const&)> const& validate,
bool testSerialized = true)
{
env.close();
Json::Value params;
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 (testSerialized)
{
// This cannot be tested in the multisign autofill scenario
// It is technically not a valid STObject, so the following line
// will crash
STParsedJSONObject const parsed(std::string(jss::tx_json), tx);
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
auto const tx_blob = strHex(parsed.object->getSerializer().peekData());
if (BEAST_EXPECT(parsed.object.has_value()))
{
Json::Value params;
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);
}
BEAST_EXPECTS(env.current()->txCount() == 0, std::to_string(env.current()->txCount()));
}
void
testTxJsonMetadataField(
jtx::Env& env,
Json::Value const& tx,
std::function<void(
Json::Value const&,
Json::Value const&,
Json::Value const&,
Json::Value const&)> const& validate,
Json::Value const& expectedMetadataKey,
Json::Value const& expectedMetadataValue)
{
env.close();
Json::Value params;
params[jss::tx_json] = tx;
validate(
env.rpc("json", "simulate", to_string(params)),
tx,
expectedMetadataKey,
expectedMetadataValue);
validate(
env.rpc("simulate", to_string(tx)), tx, expectedMetadataKey, expectedMetadataValue);
BEAST_EXPECTS(env.current()->txCount() == 0, std::to_string(env.current()->txCount()));
}
static Json::Value
getJsonMetadata(Json::Value txResult)
{
if (txResult.isMember(jss::meta_blob))
{
auto unHexed = strUnHex(txResult[jss::meta_blob].asString());
SerialIter sitTrans(makeSlice(*unHexed)); // NOLINT(bugprone-unchecked-optional-access)
return STObject(std::ref(sitTrans), sfGeneric).getJson(JsonOptions::none);
}
return txResult[jss::meta];
}
void
testParamErrors()
{
testcase("Test parameter errors");
using namespace jtx;
Env env(*this);
Account const alice("alice");
{
// No params
Json::Value const params = Json::objectValue;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(
resp[jss::result][jss::error_message] ==
"Neither `tx_blob` nor `tx_json` included.");
}
{
// Providing both `tx_json` and `tx_blob`
Json::Value params = Json::objectValue;
params[jss::tx_json] = Json::objectValue;
params[jss::tx_blob] = "1200";
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`.");
}
{
// `binary` isn't a boolean
Json::Value params = Json::objectValue;
params[jss::tx_blob] = "1200";
params[jss::binary] = "100";
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'binary'.");
}
{
// Invalid `tx_blob`
Json::Value params = Json::objectValue;
params[jss::tx_blob] = "12";
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx_blob'.");
}
{
// Empty `tx_json`
Json::Value params = Json::objectValue;
params[jss::tx_json] = Json::objectValue;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(
resp[jss::result][jss::error_message] == "Missing field 'tx.TransactionType'.");
}
{
// No tx.Account
Json::Value params = Json::objectValue;
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::Payment;
params[jss::tx_json] = tx_json;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Missing field 'tx.Account'.");
}
{
// Empty `tx_blob`
Json::Value params = Json::objectValue;
params[jss::tx_blob] = "";
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx_blob'.");
}
{
// Non-string `tx_blob`
Json::Value params;
params[jss::tx_blob] = 1.1;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx_blob'.");
}
{
// Non-object `tx_json`
Json::Value params = Json::objectValue;
params[jss::tx_json] = "";
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(
resp[jss::result][jss::error_message] == "Invalid field 'tx_json', not object.");
}
{
// `seed` field included
Json::Value params = Json::objectValue;
params[jss::seed] = "random_data";
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;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'seed'.");
}
{
// `secret` field included
Json::Value params = Json::objectValue;
params[jss::secret] = "random_data";
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;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'secret'.");
}
{
// `seed_hex` field included
Json::Value params = Json::objectValue;
params[jss::seed_hex] = "random_data";
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;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'seed_hex'.");
}
{
// `passphrase` field included
Json::Value params = Json::objectValue;
params[jss::passphrase] = "random_data";
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;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'passphrase'.");
}
{
// Invalid transaction
Json::Value params = Json::objectValue;
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::Payment;
tx_json[jss::Account] = env.master.human();
params[jss::tx_json] = tx_json;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(
resp[jss::result][jss::error_exception] ==
"Field 'Destination' is required but missing.");
}
{
// Bad account
Json::Value params;
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::AccountSet;
tx_json[jss::Account] = "badAccount";
params[jss::tx_json] = tx_json;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECTS(
resp[jss::result][jss::error] == "srcActMalformed",
resp[jss::result][jss::error].toStyledString());
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Account'.");
}
{
// Account doesn't exist for Sequence autofill
Json::Value params;
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::AccountSet;
tx_json[jss::Account] = alice.human();
params[jss::tx_json] = tx_json;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Source account not found.");
}
{
// Invalid Signers field
Json::Value params;
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::AccountSet;
tx_json[jss::Account] = env.master.human();
tx_json[sfSigners] = "1";
params[jss::tx_json] = tx_json;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Signers'.");
}
{
// Invalid Signers field
Json::Value params;
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::AccountSet;
tx_json[jss::Account] = env.master.human();
tx_json[sfSigners] = Json::arrayValue;
tx_json[sfSigners].append("1");
params[jss::tx_json] = tx_json;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Signers[0]'.");
}
{
// Invalid transaction
Json::Value params;
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::AccountSet;
tx_json[jss::Account] = env.master.human();
tx_json["foo"] = "bar";
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.foo' is unknown.");
}
{
// non-`"binary"` second param for CLI
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::AccountSet;
tx_json[jss::Account] = alice.human();
auto const resp = env.rpc("simulate", to_string(tx_json), "1");
BEAST_EXPECT(resp[jss::error_message] == "Invalid parameters.");
}
{
// Signed transaction
Json::Value params;
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::AccountSet;
tx_json[jss::Account] = env.master.human();
tx_json[jss::TxnSignature] = "1200ABCD";
params[jss::tx_json] = tx_json;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(
resp[jss::result][jss::error_message] == "Transaction should not be signed.");
}
{
// Signed multisig transaction
Json::Value params;
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::AccountSet;
tx_json[jss::Account] = env.master.human();
tx_json[sfSigners] = Json::arrayValue;
{
Json::Value signer;
signer[jss::Account] = alice.human();
signer[jss::SigningPubKey] = alice.human();
signer[jss::TxnSignature] = "1200ABCD";
Json::Value signerOuter;
signerOuter[sfSigner] = signer;
tx_json[sfSigners].append(signerOuter);
}
params[jss::tx_json] = tx_json;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(
resp[jss::result][jss::error_message] == "Transaction should not be signed.");
}
{
// tfInnerBatchTxn flag on top-level transaction
Json::Value params;
Json::Value tx_json = Json::objectValue;
tx_json[jss::TransactionType] = jss::AccountSet;
tx_json[jss::Account] = env.master.human();
tx_json[jss::Flags] = tfInnerBatchTxn;
params[jss::tx_json] = tx_json;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(
resp[jss::result][jss::error_message] ==
"tfInnerBatchTxn flag is not allowed on top-level transactions.");
}
}
void
testFeeError()
{
testcase("Fee failure");
using namespace jtx;
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->section("transaction_queue").set("minimum_txn_in_ledger_standalone", "3");
return cfg;
}));
Account const alice{"alice"};
env.fund(XRP(1000000), alice);
env.close();
// fill queue
auto metrics = env.app().getTxQ().getMetrics(*env.current());
for (int i = metrics.txInLedger; i <= metrics.txPerLedger; ++i)
env(noop(alice));
{
Json::Value params;
params[jss::tx_json] = noop(alice);
auto const resp = env.rpc("json", "simulate", to_string(params));
auto const result = resp[jss::result];
if (BEAST_EXPECT(result.isMember(jss::error)))
{
BEAST_EXPECT(result[jss::error] == "highFee");
BEAST_EXPECT(result[jss::error_code] == rpcHIGH_FEE);
}
}
}
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 + 2));
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()
{
testcase("Successful transaction");
using namespace jtx;
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
cfg->NETWORK_ID = 0;
return cfg;
})};
static auto const newDomain = "123ABC";
{
auto validateOutput = [&](Json::Value const& resp, Json::Value const& tx) {
auto result = resp[jss::result];
checkBasicReturnValidity(result, tx, 1, env.current()->fees().base);
BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS");
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");
}
};
Json::Value tx;
tx[jss::Account] = env.master.human();
tx[jss::TransactionType] = jss::AccountSet;
tx[sfDomain] = newDomain;
// test with autofill
testTx(env, tx, validateOutput);
tx[sfSigningPubKey] = "";
tx[sfTxnSignature] = "";
tx[sfSequence] = 1;
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
// test without autofill
testTx(env, tx, validateOutput);
}
}
void
testTransactionNonTecFailure()
{
testcase("Transaction non-tec failure");
using namespace jtx;
Env env(*this);
Account const alice("alice");
{
std::function<void(Json::Value const&, Json::Value const&)> const& testSimulation =
[&](Json::Value const& resp, Json::Value const& tx) {
auto result = resp[jss::result];
checkBasicReturnValidity(result, tx, 1, env.current()->fees().base);
BEAST_EXPECT(result[jss::engine_result] == "temBAD_AMOUNT");
BEAST_EXPECT(result[jss::engine_result_code] == -298);
BEAST_EXPECT(result[jss::engine_result_message] == "Malformed: Bad amount.");
BEAST_EXPECT(!result.isMember(jss::meta) && !result.isMember(jss::meta_blob));
};
Json::Value tx;
tx[jss::Account] = env.master.human();
tx[jss::TransactionType] = jss::Payment;
tx[sfDestination] = alice.human();
tx[sfAmount] = "0"; // invalid amount
// test with autofill
testTx(env, tx, testSimulation);
tx[sfSigningPubKey] = "";
tx[sfTxnSignature] = "";
tx[sfSequence] = 1;
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
// test without autofill
testTx(env, tx, testSimulation);
}
}
void
testTransactionTecFailure()
{
testcase("Transaction tec failure");
using namespace jtx;
Env env(*this);
Account const alice("alice");
{
std::function<void(Json::Value const&, Json::Value const&)> const& testSimulation =
[&](Json::Value const& resp, Json::Value const& tx) {
auto result = resp[jss::result];
checkBasicReturnValidity(result, tx, 1, env.current()->fees().base);
BEAST_EXPECT(result[jss::engine_result] == "tecNO_DST_INSUF_XRP");
BEAST_EXPECT(result[jss::engine_result_code] == 125);
BEAST_EXPECT(
result[jss::engine_result_message] ==
"Destination does not exist. Too little XRP sent to "
"create "
"it.");
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[sfBalance] ==
std::to_string(
100'000'000'000'000'000 -
env.current()->fees().base.drops()));
}
}
BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0);
BEAST_EXPECT(
metadata[sfTransactionResult.jsonName] == "tecNO_DST_INSUF_XRP");
}
};
Json::Value tx;
tx[jss::Account] = env.master.human();
tx[jss::TransactionType] = jss::Payment;
tx[sfDestination] = alice.human();
tx[sfAmount] = "1"; // not enough to create an account
// test with autofill
testTx(env, tx, testSimulation);
tx[sfSigningPubKey] = "";
tx[sfTxnSignature] = "";
tx[sfSequence] = 1;
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
// test without autofill
testTx(env, tx, testSimulation);
}
}
void
testSuccessfulTransactionMultisigned()
{
testcase("Successful multi-signed transaction");
using namespace jtx;
Env env(*this);
static auto const newDomain = "123ABC";
Account const alice("alice");
Account const becky("becky");
Account const carol("carol");
env.fund(XRP(10000), alice);
env.close();
// set up valid multisign
env(signers(alice, 1, {{becky, 1}, {carol, 1}}));
env.close();
{
auto validateOutput = [&](Json::Value const& resp, Json::Value const& tx) {
auto result = resp[jss::result];
checkBasicReturnValidity(
result,
tx,
env.seq(alice),
tx.isMember(jss::Signers) ? env.current()->fees().base * 2
: env.current()->fees().base);
BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS");
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");
}
};
Json::Value tx;
tx[jss::Account] = alice.human();
tx[jss::TransactionType] = jss::AccountSet;
tx[sfDomain] = newDomain;
// test with autofill
testTx(env, tx, validateOutput, false);
tx[sfSigners] = Json::arrayValue;
{
Json::Value signer;
signer[jss::Account] = becky.human();
Json::Value signerOuter;
signerOuter[sfSigner] = signer;
tx[sfSigners].append(signerOuter);
}
// test with just signer accounts
testTx(env, tx, validateOutput, false);
tx[sfSigningPubKey] = "";
tx[sfTxnSignature] = "";
tx[sfSequence] = env.seq(alice);
// transaction requires a non-base fee
tx[sfFee] = (env.current()->fees().base * 2).jsonClipped().asString();
tx[sfSigners][0u][sfSigner][jss::SigningPubKey] = "";
tx[sfSigners][0u][sfSigner][jss::TxnSignature] = "";
// test without autofill
testTx(env, tx, validateOutput);
}
}
void
testTransactionSigningFailure()
{
testcase("Transaction with a key-related failure");
using namespace jtx;
Env env(*this);
static auto const newDomain = "123ABC";
Account const alice{"alice"};
env(regkey(env.master, alice));
env(fset(env.master, asfDisableMaster), sig(env.master));
env.close();
{
std::function<void(Json::Value const&, Json::Value const&)> const& testSimulation =
[&](Json::Value const& resp, Json::Value const& tx) {
auto result = resp[jss::result];
checkBasicReturnValidity(
result, tx, env.seq(env.master), env.current()->fees().base);
BEAST_EXPECT(result[jss::engine_result] == "tefMASTER_DISABLED");
BEAST_EXPECT(result[jss::engine_result_code] == -188);
BEAST_EXPECT(result[jss::engine_result_message] == "Master key is disabled.");
BEAST_EXPECT(!result.isMember(jss::meta) && !result.isMember(jss::meta_blob));
};
Json::Value tx;
tx[jss::Account] = env.master.human();
tx[jss::TransactionType] = jss::AccountSet;
tx[sfDomain] = newDomain;
// master key is disabled, so this is invalid
tx[jss::SigningPubKey] = strHex(env.master.pk().slice());
// test with autofill
testTx(env, tx, testSimulation);
tx[sfTxnSignature] = "";
tx[sfSequence] = env.seq(env.master);
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
// test without autofill
testTx(env, tx, testSimulation);
}
}
void
testInvalidSingleAndMultiSigningTransaction()
{
testcase(
"Transaction with both single-signing SigningPubKey and "
"multi-signing Signers");
using namespace jtx;
Env env(*this);
static auto const newDomain = "123ABC";
Account const alice("alice");
Account const becky("becky");
Account const carol("carol");
env.fund(XRP(10000), alice);
env.close();
// set up valid multisign
env(signers(alice, 1, {{becky, 1}, {carol, 1}}));
env.close();
{
std::function<void(Json::Value const&, Json::Value const&)> const& testSimulation =
[&](Json::Value const& resp, Json::Value const& tx) {
auto result = resp[jss::result];
checkBasicReturnValidity(
result, tx, env.seq(env.master), env.current()->fees().base * 2);
BEAST_EXPECT(result[jss::engine_result] == "temINVALID");
BEAST_EXPECT(result[jss::engine_result_code] == -277);
BEAST_EXPECT(
result[jss::engine_result_message] == "The transaction is ill-formed.");
BEAST_EXPECT(!result.isMember(jss::meta) && !result.isMember(jss::meta_blob));
};
Json::Value tx;
tx[jss::Account] = env.master.human();
tx[jss::TransactionType] = jss::AccountSet;
tx[sfDomain] = newDomain;
// master key is disabled, so this is invalid
tx[jss::SigningPubKey] = strHex(env.master.pk().slice());
tx[sfSigners] = Json::arrayValue;
{
Json::Value signer;
signer[jss::Account] = becky.human();
Json::Value signerOuter;
signerOuter[sfSigner] = signer;
tx[sfSigners].append(signerOuter);
}
// test with autofill
testTx(env, tx, testSimulation, false);
tx[sfTxnSignature] = "";
tx[sfSequence] = env.seq(env.master);
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
tx[sfSigners][0u][sfSigner][jss::SigningPubKey] = strHex(becky.pk().slice());
tx[sfSigners][0u][sfSigner][jss::TxnSignature] = "";
// test without autofill
testTx(env, tx, testSimulation);
}
}
void
testMultisignedBadPubKey()
{
testcase("Multi-signed transaction with a bad public key");
using namespace jtx;
Env env(*this);
static auto const newDomain = "123ABC";
Account const alice("alice");
Account const becky("becky");
Account const carol("carol");
Account const dylan("dylan");
env.fund(XRP(10000), alice);
env.close();
// set up valid multisign
env(signers(alice, 1, {{becky, 1}, {carol, 1}}));
{
auto validateOutput = [&](Json::Value const& resp, Json::Value const& tx) {
auto result = resp[jss::result];
checkBasicReturnValidity(
result, tx, env.seq(alice), env.current()->fees().base * 2);
BEAST_EXPECTS(
result[jss::engine_result] == "tefBAD_SIGNATURE",
result[jss::engine_result].toStyledString());
BEAST_EXPECT(result[jss::engine_result_code] == -186);
BEAST_EXPECT(
result[jss::engine_result_message] ==
"A signature is provided for a non-signer.");
BEAST_EXPECT(!result.isMember(jss::meta) && !result.isMember(jss::meta_blob));
};
Json::Value tx;
tx[jss::Account] = alice.human();
tx[jss::TransactionType] = jss::AccountSet;
tx[sfDomain] = newDomain;
tx[sfSigners] = Json::arrayValue;
{
Json::Value signer;
signer[jss::Account] = becky.human();
signer[jss::SigningPubKey] = strHex(dylan.pk().slice());
Json::Value signerOuter;
signerOuter[sfSigner] = signer;
tx[sfSigners].append(signerOuter);
}
// test with autofill
testTx(env, tx, validateOutput, false);
tx[sfSigningPubKey] = "";
tx[sfTxnSignature] = "";
tx[sfSequence] = env.seq(alice);
// transaction requires a non-base fee
tx[sfFee] = (env.current()->fees().base * 2).jsonClipped().asString();
tx[sfSigners][0u][sfSigner][jss::TxnSignature] = "";
// test without autofill
testTx(env, tx, validateOutput);
}
}
void
testDeleteExpiredCredentials()
{
testcase("Credentials aren't actually deleted on `tecEXPIRED`");
// scenario setup
using namespace jtx;
Env env(*this);
Account const subject{"subject"};
Account const issuer{"issuer"};
env.fund(XRP(10000), subject, issuer);
env.close();
auto const credType = "123ABC";
auto jv = credentials::create(subject, issuer, credType);
uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count();
jv[sfExpiration.jsonName] = t;
env(jv);
env.close();
{
auto validateOutput = [&](Json::Value const& resp, Json::Value const& tx) {
auto result = resp[jss::result];
checkBasicReturnValidity(result, tx, env.seq(subject), env.current()->fees().base);
BEAST_EXPECT(result[jss::engine_result] == "tecEXPIRED");
BEAST_EXPECT(result[jss::engine_result_code] == 148);
BEAST_EXPECT(result[jss::engine_result_message] == "Expiration time is passed.");
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() == 5);
try
{
bool found = false;
for (auto const& node : metadata[sfAffectedNodes.jsonName])
{
if (node.isMember(sfDeletedNode.jsonName) &&
node[sfDeletedNode.jsonName][sfLedgerEntryType.jsonName]
.asString() == "Credential")
{
auto const deleted =
node[sfDeletedNode.jsonName][sfFinalFields.jsonName];
found = deleted[jss::Issuer] == issuer.human() &&
deleted[jss::Subject] == subject.human() &&
deleted["CredentialType"] ==
strHex(std::string_view(credType));
break;
}
}
BEAST_EXPECT(found);
}
catch (...)
{
fail();
}
}
BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0);
BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tecEXPIRED");
}
};
Json::Value tx = credentials::accept(subject, issuer, credType);
// test with autofill
testTx(env, tx, validateOutput);
tx[sfSigningPubKey] = "";
tx[sfTxnSignature] = "";
tx[sfSequence] = env.seq(subject);
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
// test without autofill
testTx(env, tx, validateOutput);
}
// check that expired credentials weren't deleted
auto const jle = credentials::ledgerEntry(env, subject, issuer, credType);
BEAST_EXPECT(
jle.isObject() && jle.isMember(jss::result) && !jle[jss::result].isMember(jss::error) &&
jle[jss::result].isMember(jss::node) &&
jle[jss::result][jss::node].isMember("LedgerEntryType") &&
jle[jss::result][jss::node]["LedgerEntryType"] == jss::Credential &&
jle[jss::result][jss::node][jss::Issuer] == issuer.human() &&
jle[jss::result][jss::node][jss::Subject] == subject.human() &&
jle[jss::result][jss::node]["CredentialType"] == strHex(std::string_view(credType)));
BEAST_EXPECT(ownerCount(env, issuer) == 1);
BEAST_EXPECT(ownerCount(env, subject) == 0);
}
void
testSuccessfulTransactionNetworkID()
{
testcase("Successful transaction with a custom network ID");
using namespace jtx;
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
cfg->NETWORK_ID = 1025;
return cfg;
})};
static auto const newDomain = "123ABC";
{
auto validateOutput = [&](Json::Value const& resp, Json::Value const& tx) {
auto result = resp[jss::result];
checkBasicReturnValidity(result, tx, 1, env.current()->fees().base);
BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS");
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");
}
};
Json::Value tx;
tx[jss::Account] = env.master.human();
tx[jss::TransactionType] = jss::AccountSet;
tx[sfDomain] = newDomain;
// test with autofill
testTx(env, tx, validateOutput);
tx[sfSigningPubKey] = "";
tx[sfTxnSignature] = "";
tx[sfSequence] = 1;
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
tx[sfNetworkID] = 1025;
// test without autofill
testTx(env, tx, validateOutput);
}
}
void
testSuccessfulTransactionAdditionalMetadata()
{
testcase("Successful transaction with additional metadata");
using namespace jtx;
using namespace std::chrono_literals;
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
cfg->NETWORK_ID = 1025;
return cfg;
})};
Account const alice("alice");
Account const bob("bob");
env.fund(XRP(10000), alice, bob);
env.close();
// deliver_amount is unavailable in the metadata before 2014-02-01
// so proceed to 2014-02-01
env.close(NetClock::time_point{446000000s});
{
auto validateOutput = [&](Json::Value const& resp,
Json::Value const& tx,
Json::Value const& expectedMetadataKey,
Json::Value const& expectedMetadataValue) {
auto result = resp[jss::result];
BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS");
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);
BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0);
BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS");
BEAST_EXPECT(metadata.isMember(expectedMetadataKey.asString()));
BEAST_EXPECT(metadata[expectedMetadataKey.asString()] == expectedMetadataValue);
}
};
{
Json::Value tx;
tx[jss::Account] = alice.human();
tx[jss::TransactionType] = jss::Payment;
tx[sfDestination] = bob.human();
tx[sfAmount] = "100";
// test delivered amount
testTxJsonMetadataField(env, tx, validateOutput, jss::delivered_amount, "100");
}
{
Json::Value tx;
tx[jss::Account] = alice.human();
tx[jss::TransactionType] = jss::NFTokenMint;
tx[sfNFTokenTaxon] = 1;
Json::Value const nftokenId = to_string(token::getNextID(env, alice, 1));
// test nft synthetic
testTxJsonMetadataField(env, tx, validateOutput, jss::nftoken_id, nftokenId);
}
{
Json::Value tx;
tx[jss::Account] = alice.human();
tx[jss::TransactionType] = jss::MPTokenIssuanceCreate;
Json::Value const mptIssuanceId = to_string(makeMptID(env.seq(alice), alice));
// test mpt issuance id
testTxJsonMetadataField(
env, tx, validateOutput, jss::mpt_issuance_id, mptIssuanceId);
}
}
}
public:
void
run() override
{
testParamErrors();
testFeeError();
testInvalidTransactionType();
testSuccessfulTransaction();
testTransactionNonTecFailure();
testTransactionTecFailure();
testSuccessfulTransactionMultisigned();
testTransactionSigningFailure();
testInvalidSingleAndMultiSigningTransaction();
testMultisignedBadPubKey();
testDeleteExpiredCredentials();
testSuccessfulTransactionNetworkID();
testSuccessfulTransactionAdditionalMetadata();
}
};
BEAST_DEFINE_TESTSUITE(Simulate, rpc, xrpl);
} // namespace test
} // namespace xrpl