Files
xahaud/src/test/app/MultiSign_test.cpp
Richard Holland 6106c356bb clang
2026-01-31 18:01:23 +11:00

2536 lines
95 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/core/ConfigSections.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
namespace ripple {
namespace test {
class MultiSign_test : public beast::unit_test::suite
{
// Unfunded accounts to use for phantom signing.
jtx::Account const bogie{"bogie", KeyType::secp256k1};
jtx::Account const demon{"demon", KeyType::ed25519};
jtx::Account const ghost{"ghost", KeyType::secp256k1};
jtx::Account const haunt{"haunt", KeyType::ed25519};
jtx::Account const jinni{"jinni", KeyType::secp256k1};
jtx::Account const phase{"phase", KeyType::ed25519};
jtx::Account const shade{"shade", KeyType::secp256k1};
jtx::Account const spook{"spook", KeyType::ed25519};
jtx::Account const acc10{"acc10", KeyType::ed25519};
jtx::Account const acc11{"acc11", KeyType::ed25519};
jtx::Account const acc12{"acc12", KeyType::ed25519};
jtx::Account const acc13{"acc13", KeyType::ed25519};
jtx::Account const acc14{"acc14", KeyType::ed25519};
jtx::Account const acc15{"acc15", KeyType::ed25519};
jtx::Account const acc16{"acc16", KeyType::ed25519};
jtx::Account const acc17{"acc17", KeyType::ed25519};
jtx::Account const acc18{"acc18", KeyType::ed25519};
jtx::Account const acc19{"acc19", KeyType::ed25519};
jtx::Account const acc20{"acc20", KeyType::ed25519};
jtx::Account const acc21{"acc21", KeyType::ed25519};
jtx::Account const acc22{"acc22", KeyType::ed25519};
jtx::Account const acc23{"acc23", KeyType::ed25519};
jtx::Account const acc24{"acc24", KeyType::ed25519};
jtx::Account const acc25{"acc25", KeyType::ed25519};
jtx::Account const acc26{"acc26", KeyType::ed25519};
jtx::Account const acc27{"acc27", KeyType::ed25519};
jtx::Account const acc28{"acc28", KeyType::ed25519};
jtx::Account const acc29{"acc29", KeyType::ed25519};
jtx::Account const acc30{"acc30", KeyType::ed25519};
jtx::Account const acc31{"acc31", KeyType::ed25519};
jtx::Account const acc32{"acc32", KeyType::ed25519};
jtx::Account const acc33{"acc33", KeyType::ed25519};
public:
void
test_noReserve(FeatureBitset features)
{
testcase("No Reserve");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::secp256k1};
// The reserve required for a signer list changes with the passage
// of featureMultiSignReserve. Make the required adjustments.
bool const reserve1{features[featureMultiSignReserve]};
// Pay alice enough to meet the initial reserve, but not enough to
// meet the reserve for a SignerListSet.
auto const fee = env.current()->fees().base;
auto const smallSignersReserve = reserve1 ? XRP(250) : XRP(350);
env.fund(smallSignersReserve - drops(1), alice);
env.close();
env.require(owners(alice, 0));
{
// Attach a signer list to alice. Should fail.
Json::Value smallSigners = signers(alice, 1, {{bogie, 1}});
env(smallSigners, ter(tecINSUFFICIENT_RESERVE));
env.close();
env.require(owners(alice, 0));
// Fund alice enough to set the signer list, then attach signers.
env(pay(env.master, alice, fee + drops(1)));
env.close();
env(smallSigners);
env.close();
env.require(owners(alice, reserve1 ? 1 : 3));
}
{
// Pay alice enough to almost make the reserve for the biggest
// possible list.
auto const addReserveBigSigners = reserve1 ? XRP(0) : XRP(350);
env(pay(env.master, alice, addReserveBigSigners + fee - drops(1)));
// Replace with the biggest possible signer list. Should fail.
Json::Value bigSigners = signers(
alice,
1,
{{bogie, 1},
{demon, 1},
{ghost, 1},
{haunt, 1},
{jinni, 1},
{phase, 1},
{shade, 1},
{spook, 1}});
env(bigSigners, ter(tecINSUFFICIENT_RESERVE));
env.close();
env.require(owners(alice, reserve1 ? 1 : 3));
// Fund alice one more drop (plus the fee) and succeed.
env(pay(env.master, alice, fee + drops(1)));
env.close();
env(bigSigners);
env.close();
env.require(owners(alice, reserve1 ? 1 : 10));
}
// Remove alice's signer list and get the owner count back.
env(signers(alice, jtx::none));
env.close();
env.require(owners(alice, 0));
}
void
test_signerListSet(FeatureBitset features)
{
testcase("SignerListSet");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::ed25519};
env.fund(XRP(1000), alice);
// Add alice as a multisigner for herself. Should fail.
env(signers(alice, 1, {{alice, 1}}), ter(temBAD_SIGNER));
// Add a signer with a weight of zero. Should fail.
env(signers(alice, 1, {{bogie, 0}}), ter(temBAD_WEIGHT));
// Add a signer where the weight is too big. Should fail since
// the weight field is only 16 bits. The jtx framework can't do
// this kind of test, so it's commented out.
// env(signers(alice, 1, { { bogie, 0x10000} }), ter
// (temBAD_WEIGHT));
// Add the same signer twice. Should fail.
env(signers(
alice,
1,
{{bogie, 1},
{demon, 1},
{ghost, 1},
{haunt, 1},
{jinni, 1},
{phase, 1},
{demon, 1},
{spook, 1}}),
ter(temBAD_SIGNER));
// Set a quorum of zero. Should fail.
env(signers(alice, 0, {{bogie, 1}}), ter(temMALFORMED));
// Make a signer list where the quorum can't be met. Should fail.
env(signers(
alice,
9,
{{bogie, 1},
{demon, 1},
{ghost, 1},
{haunt, 1},
{jinni, 1},
{phase, 1},
{shade, 1},
{spook, 1}}),
ter(temBAD_QUORUM));
// Make a signer list that's too big. Should fail. (Even with
// ExpandedSignerList)
Account const spare("spare", KeyType::secp256k1);
env(signers(
alice,
1,
features[featureExpandedSignerList]
? std::vector<signer>{{bogie, 1}, {demon, 1}, {ghost, 1},
{haunt, 1}, {jinni, 1}, {phase, 1},
{shade, 1}, {spook, 1}, {spare, 1},
{acc10, 1}, {acc11, 1}, {acc12, 1},
{acc13, 1}, {acc14, 1}, {acc15, 1},
{acc16, 1}, {acc17, 1}, {acc18, 1},
{acc19, 1}, {acc20, 1}, {acc21, 1},
{acc22, 1}, {acc23, 1}, {acc24, 1},
{acc25, 1}, {acc26, 1}, {acc27, 1},
{acc28, 1}, {acc29, 1}, {acc30, 1},
{acc31, 1}, {acc32, 1}, {acc33, 1}}
: std::vector<
signer>{{bogie, 1}, {demon, 1}, {ghost, 1}, {haunt, 1}, {jinni, 1}, {phase, 1}, {shade, 1}, {spook, 1}, {spare, 1}}),
ter(temMALFORMED));
// clang-format on
env.close();
env.require(owners(alice, 0));
}
void
test_phantomSigners(FeatureBitset features)
{
testcase("Phantom Signers");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::ed25519};
env.fund(XRP(1000), alice);
env.close();
// Attach phantom signers to alice and use them for a transaction.
env(signers(alice, 1, {{bogie, 1}, {demon, 1}}));
env.close();
env.require(owners(alice, features[featureMultiSignReserve] ? 1 : 4));
// This should work.
auto const baseFee = env.current()->fees().base;
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice), msig(bogie, demon), fee(3 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Either signer alone should work.
aliceSeq = env.seq(alice);
env(noop(alice), msig(bogie), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
aliceSeq = env.seq(alice);
env(noop(alice), msig(demon), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Duplicate signers should fail.
aliceSeq = env.seq(alice);
env(noop(alice), msig(demon, demon), fee(3 * baseFee), ter(temINVALID));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// A non-signer should fail.
aliceSeq = env.seq(alice);
env(noop(alice),
msig(bogie, spook),
fee(3 * baseFee),
ter(tefBAD_SIGNATURE));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Don't meet the quorum. Should fail.
env(signers(alice, 2, {{bogie, 1}, {demon, 1}}));
aliceSeq = env.seq(alice);
env(noop(alice), msig(bogie), fee(2 * baseFee), ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Meet the quorum. Should succeed.
aliceSeq = env.seq(alice);
env(noop(alice), msig(bogie, demon), fee(3 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
void
test_fee(FeatureBitset features)
{
testcase("Fee");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::ed25519};
env.fund(XRP(1000), alice);
env.close();
// Attach maximum possible number of signers to alice.
env(signers(
alice,
1,
{{bogie, 1},
{demon, 1},
{ghost, 1},
{haunt, 1},
{jinni, 1},
{phase, 1},
{shade, 1},
{spook, 1}}));
env.close();
env.require(owners(alice, features[featureMultiSignReserve] ? 1 : 10));
// This should work.
auto const baseFee = env.current()->fees().base;
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice), msig(bogie), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// This should fail because the fee is too small.
aliceSeq = env.seq(alice);
env(noop(alice),
msig(bogie),
fee((2 * baseFee) - 1),
ter(telINSUF_FEE_P));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// This should work.
aliceSeq = env.seq(alice);
env(noop(alice),
msig(bogie, demon, ghost, haunt, jinni, phase, shade, spook),
fee(9 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// This should fail because the fee is too small.
aliceSeq = env.seq(alice);
env(noop(alice),
msig(bogie, demon, ghost, haunt, jinni, phase, shade, spook),
fee((9 * baseFee) - 1),
ter(telINSUF_FEE_P));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
void
test_misorderedSigners(FeatureBitset features)
{
testcase("Misordered Signers");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::ed25519};
env.fund(XRP(1000), alice);
env.close();
// The signatures in a transaction must be submitted in sorted order.
// Make sure the transaction fails if they are not.
env(signers(alice, 1, {{bogie, 1}, {demon, 1}}));
env.close();
env.require(owners(alice, features[featureMultiSignReserve] ? 1 : 4));
msig phantoms{bogie, demon};
std::reverse(phantoms.signers.begin(), phantoms.signers.end());
std::uint32_t const aliceSeq = env.seq(alice);
env(noop(alice), phantoms, ter(temINVALID));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
void
test_masterSigners(FeatureBitset features)
{
testcase("Master Signers");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::ed25519};
Account const becky{"becky", KeyType::secp256k1};
Account const cheri{"cheri", KeyType::ed25519};
env.fund(XRP(1000), alice, becky, cheri);
env.close();
// For a different situation, give alice a regular key but don't use it.
Account const alie{"alie", KeyType::secp256k1};
env(regkey(alice, alie));
env.close();
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice), sig(alice));
env(noop(alice), sig(alie));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 2);
// Attach signers to alice
env(signers(alice, 4, {{becky, 3}, {cheri, 4}}), sig(alice));
env.close();
env.require(owners(alice, features[featureMultiSignReserve] ? 1 : 4));
// Attempt a multisigned transaction that meets the quorum.
auto const baseFee = env.current()->fees().base;
aliceSeq = env.seq(alice);
env(noop(alice), msig(cheri), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// If we don't meet the quorum the transaction should fail.
aliceSeq = env.seq(alice);
env(noop(alice), msig(becky), fee(2 * baseFee), ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Give becky and cheri regular keys.
Account const beck{"beck", KeyType::ed25519};
env(regkey(becky, beck));
Account const cher{"cher", KeyType::ed25519};
env(regkey(cheri, cher));
env.close();
// becky's and cheri's master keys should still work.
aliceSeq = env.seq(alice);
env(noop(alice), msig(becky, cheri), fee(3 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
void
test_regularSigners(FeatureBitset features)
{
testcase("Regular Signers");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::secp256k1};
Account const becky{"becky", KeyType::ed25519};
Account const cheri{"cheri", KeyType::secp256k1};
env.fund(XRP(1000), alice, becky, cheri);
env.close();
// Attach signers to alice.
env(signers(alice, 1, {{becky, 1}, {cheri, 1}}), sig(alice));
// Give everyone regular keys.
Account const alie{"alie", KeyType::ed25519};
env(regkey(alice, alie));
Account const beck{"beck", KeyType::secp256k1};
env(regkey(becky, beck));
Account const cher{"cher", KeyType::ed25519};
env(regkey(cheri, cher));
env.close();
// Disable cheri's master key to mix things up.
env(fset(cheri, asfDisableMaster), sig(cheri));
env.close();
// Attempt a multisigned transaction that meets the quorum.
auto const baseFee = env.current()->fees().base;
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice), msig(msig::Reg{cheri, cher}), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// cheri should not be able to multisign using her master key.
aliceSeq = env.seq(alice);
env(noop(alice),
msig(cheri),
fee(2 * baseFee),
ter(tefMASTER_DISABLED));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// becky should be able to multisign using either of her keys.
aliceSeq = env.seq(alice);
env(noop(alice), msig(becky), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
aliceSeq = env.seq(alice);
env(noop(alice), msig(msig::Reg{becky, beck}), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Both becky and cheri should be able to sign using regular keys.
aliceSeq = env.seq(alice);
env(noop(alice),
fee(3 * baseFee),
msig(msig::Reg{becky, beck}, msig::Reg{cheri, cher}));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
void
test_regularSignersUsingSubmitMulti(FeatureBitset features)
{
testcase("Regular Signers Using submit_multisigned");
using namespace jtx;
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->loadFromString("[" SECTION_SIGNING_SUPPORT "]\ntrue");
return cfg;
}),
features);
Account const alice{"alice", KeyType::secp256k1};
Account const becky{"becky", KeyType::ed25519};
Account const cheri{"cheri", KeyType::secp256k1};
env.fund(XRP(1000), alice, becky, cheri);
env.close();
// Attach signers to alice.
env(signers(alice, 2, {{becky, 1}, {cheri, 1}}), sig(alice));
// Give everyone regular keys.
Account const beck{"beck", KeyType::secp256k1};
env(regkey(becky, beck));
Account const cher{"cher", KeyType::ed25519};
env(regkey(cheri, cher));
env.close();
// Disable cheri's master key to mix things up.
env(fset(cheri, asfDisableMaster), sig(cheri));
env.close();
auto const baseFee = env.current()->fees().base;
std::uint32_t aliceSeq;
// these represent oft-repeated setup for input json below
auto setup_tx = [&]() -> Json::Value {
Json::Value jv;
jv[jss::tx_json][jss::Account] = alice.human();
jv[jss::tx_json][jss::TransactionType] = jss::AccountSet;
jv[jss::tx_json][jss::Fee] = (8 * baseFee).jsonClipped();
jv[jss::tx_json][jss::Sequence] = env.seq(alice);
jv[jss::tx_json][jss::SigningPubKey] = "";
return jv;
};
auto cheri_sign = [&](Json::Value& jv) {
jv[jss::account] = cheri.human();
jv[jss::key_type] = "ed25519";
jv[jss::passphrase] = cher.name();
};
auto becky_sign = [&](Json::Value& jv) {
jv[jss::account] = becky.human();
jv[jss::secret] = beck.name();
};
{
// Attempt a multisigned transaction that meets the quorum.
// using sign_for and submit_multisigned
aliceSeq = env.seq(alice);
Json::Value jv_one = setup_tx();
cheri_sign(jv_one);
auto jrr =
env.rpc("json", "sign_for", to_string(jv_one))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
// for the second sign_for, use the returned tx_json with
// first signer info
Json::Value jv_two;
jv_two[jss::tx_json] = jrr[jss::tx_json];
becky_sign(jv_two);
jrr = env.rpc("json", "sign_for", to_string(jv_two))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
Json::Value jv_submit;
jv_submit[jss::tx_json] = jrr[jss::tx_json];
jrr = env.rpc(
"json",
"submit_multisigned",
to_string(jv_submit))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
{
// failure case -- SigningPubKey not empty
aliceSeq = env.seq(alice);
Json::Value jv_one = setup_tx();
jv_one[jss::tx_json][jss::SigningPubKey] =
strHex(alice.pk().slice());
cheri_sign(jv_one);
auto jrr =
env.rpc("json", "sign_for", to_string(jv_one))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "error");
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(
jrr[jss::error_message] ==
"When multi-signing 'tx_json.SigningPubKey' must be empty.");
}
{
// failure case - bad fee
aliceSeq = env.seq(alice);
Json::Value jv_one = setup_tx();
jv_one[jss::tx_json][jss::Fee] = -1;
cheri_sign(jv_one);
auto jrr =
env.rpc("json", "sign_for", to_string(jv_one))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
// for the second sign_for, use the returned tx_json with
// first signer info
Json::Value jv_two;
jv_two[jss::tx_json] = jrr[jss::tx_json];
becky_sign(jv_two);
jrr = env.rpc("json", "sign_for", to_string(jv_two))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
Json::Value jv_submit;
jv_submit[jss::tx_json] = jrr[jss::tx_json];
jrr = env.rpc(
"json",
"submit_multisigned",
to_string(jv_submit))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "error");
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(
jrr[jss::error_message] ==
"Invalid Fee field. Fees must be greater than zero.");
}
{
// failure case - bad fee v2
aliceSeq = env.seq(alice);
Json::Value jv_one = setup_tx();
jv_one[jss::tx_json][jss::Fee] =
alice["USD"](10).value().getFullText();
cheri_sign(jv_one);
auto jrr =
env.rpc("json", "sign_for", to_string(jv_one))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
// for the second sign_for, use the returned tx_json with
// first signer info
Json::Value jv_two;
jv_two[jss::tx_json] = jrr[jss::tx_json];
becky_sign(jv_two);
jrr = env.rpc("json", "sign_for", to_string(jv_two))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
Json::Value jv_submit;
jv_submit[jss::tx_json] = jrr[jss::tx_json];
jrr = env.rpc(
"json",
"submit_multisigned",
to_string(jv_submit))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "error");
BEAST_EXPECT(jrr[jss::error] == "internal");
BEAST_EXPECT(jrr[jss::error_message] == "Internal error.");
}
{
// cheri should not be able to multisign using her master key.
aliceSeq = env.seq(alice);
Json::Value jv = setup_tx();
jv[jss::account] = cheri.human();
jv[jss::secret] = cheri.name();
auto jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "error");
BEAST_EXPECT(jrr[jss::error] == "masterDisabled");
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
{
// Unlike cheri, becky should also be able to sign using her master
// key
aliceSeq = env.seq(alice);
Json::Value jv_one = setup_tx();
cheri_sign(jv_one);
auto jrr =
env.rpc("json", "sign_for", to_string(jv_one))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
// for the second sign_for, use the returned tx_json with
// first signer info
Json::Value jv_two;
jv_two[jss::tx_json] = jrr[jss::tx_json];
jv_two[jss::account] = becky.human();
jv_two[jss::key_type] = "ed25519";
jv_two[jss::passphrase] = becky.name();
jrr = env.rpc("json", "sign_for", to_string(jv_two))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
Json::Value jv_submit;
jv_submit[jss::tx_json] = jrr[jss::tx_json];
jrr = env.rpc(
"json",
"submit_multisigned",
to_string(jv_submit))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
{
// check for bad or bogus accounts in the tx
Json::Value jv = setup_tx();
jv[jss::tx_json][jss::Account] = "DEADBEEF";
cheri_sign(jv);
auto jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "error");
BEAST_EXPECT(jrr[jss::error] == "srcActMalformed");
Account const jimmy{"jimmy"};
jv[jss::tx_json][jss::Account] = jimmy.human();
jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "error");
BEAST_EXPECT(jrr[jss::error] == "srcActNotFound");
}
{
aliceSeq = env.seq(alice);
Json::Value jv = setup_tx();
jv[jss::tx_json][sfSigners.fieldName] =
Json::Value{Json::arrayValue};
becky_sign(jv);
auto jrr = env.rpc(
"json", "submit_multisigned", to_string(jv))[jss::result];
BEAST_EXPECT(jrr[jss::status] == "error");
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(
jrr[jss::error_message] ==
"tx_json.Signers array may not be empty.");
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
}
void
test_heterogeneousSigners(FeatureBitset features)
{
testcase("Heterogenious Signers");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::secp256k1};
Account const becky{"becky", KeyType::ed25519};
Account const cheri{"cheri", KeyType::secp256k1};
Account const daria{"daria", KeyType::ed25519};
env.fund(XRP(1000), alice, becky, cheri, daria);
env.close();
// alice uses a regular key with the master disabled.
Account const alie{"alie", KeyType::secp256k1};
env(regkey(alice, alie));
env(fset(alice, asfDisableMaster), sig(alice));
// becky is master only without a regular key.
// cheri has a regular key, but leaves the master key enabled.
Account const cher{"cher", KeyType::secp256k1};
env(regkey(cheri, cher));
// daria has a regular key and disables her master key.
Account const dari{"dari", KeyType::ed25519};
env(regkey(daria, dari));
env(fset(daria, asfDisableMaster), sig(daria));
env.close();
// Attach signers to alice.
env(signers(alice, 1, {{becky, 1}, {cheri, 1}, {daria, 1}, {jinni, 1}}),
sig(alie));
env.close();
env.require(owners(alice, features[featureMultiSignReserve] ? 1 : 6));
// Each type of signer should succeed individually.
auto const baseFee = env.current()->fees().base;
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice), msig(becky), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
aliceSeq = env.seq(alice);
env(noop(alice), msig(cheri), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
aliceSeq = env.seq(alice);
env(noop(alice), msig(msig::Reg{cheri, cher}), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
aliceSeq = env.seq(alice);
env(noop(alice), msig(msig::Reg{daria, dari}), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
aliceSeq = env.seq(alice);
env(noop(alice), msig(jinni), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Should also work if all signers sign.
aliceSeq = env.seq(alice);
env(noop(alice),
fee(5 * baseFee),
msig(becky, msig::Reg{cheri, cher}, msig::Reg{daria, dari}, jinni));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Require all signers to sign.
env(signers(
alice,
0x3FFFC,
{{becky, 0xFFFF},
{cheri, 0xFFFF},
{daria, 0xFFFF},
{jinni, 0xFFFF}}),
sig(alie));
env.close();
env.require(owners(alice, features[featureMultiSignReserve] ? 1 : 6));
aliceSeq = env.seq(alice);
env(noop(alice),
fee(9 * baseFee),
msig(becky, msig::Reg{cheri, cher}, msig::Reg{daria, dari}, jinni));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Try cheri with both key types.
aliceSeq = env.seq(alice);
env(noop(alice),
fee(5 * baseFee),
msig(becky, cheri, msig::Reg{daria, dari}, jinni));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Makes sure the maximum allowed number of signers works.
env(signers(
alice,
0x7FFF8,
{{becky, 0xFFFF},
{cheri, 0xFFFF},
{daria, 0xFFFF},
{haunt, 0xFFFF},
{jinni, 0xFFFF},
{phase, 0xFFFF},
{shade, 0xFFFF},
{spook, 0xFFFF}}),
sig(alie));
env.close();
env.require(owners(alice, features[featureMultiSignReserve] ? 1 : 10));
aliceSeq = env.seq(alice);
env(noop(alice),
fee(9 * baseFee),
msig(
becky,
msig::Reg{cheri, cher},
msig::Reg{daria, dari},
haunt,
jinni,
phase,
shade,
spook));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// One signer short should fail.
aliceSeq = env.seq(alice);
env(noop(alice),
msig(becky, cheri, haunt, jinni, phase, shade, spook),
fee(8 * baseFee),
ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Remove alice's signer list and get the owner count back.
env(signers(alice, jtx::none), sig(alie));
env.close();
env.require(owners(alice, 0));
}
// We want to always leave an account signable. Make sure the that we
// disallow removing the last way a transaction may be signed.
void
test_keyDisable(FeatureBitset features)
{
testcase("Key Disable");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::ed25519};
env.fund(XRP(1000), alice);
// There are three negative tests we need to make:
// M0. A lone master key cannot be disabled.
// R0. A lone regular key cannot be removed.
// L0. A lone signer list cannot be removed.
//
// Additionally, there are 6 positive tests we need to make:
// M1. The master key can be disabled if there's a regular key.
// M2. The master key can be disabled if there's a signer list.
//
// R1. The regular key can be removed if there's a signer list.
// R2. The regular key can be removed if the master key is enabled.
//
// L1. The signer list can be removed if the master key is enabled.
// L2. The signer list can be removed if there's a regular key.
// Master key tests.
// M0: A lone master key cannot be disabled.
env(fset(alice, asfDisableMaster),
sig(alice),
ter(tecNO_ALTERNATIVE_KEY));
// Add a regular key.
Account const alie{"alie", KeyType::ed25519};
env(regkey(alice, alie));
// M1: The master key can be disabled if there's a regular key.
env(fset(alice, asfDisableMaster), sig(alice));
// R0: A lone regular key cannot be removed.
env(regkey(alice, disabled), sig(alie), ter(tecNO_ALTERNATIVE_KEY));
// Add a signer list.
env(signers(alice, 1, {{bogie, 1}}), sig(alie));
// R1: The regular key can be removed if there's a signer list.
env(regkey(alice, disabled), sig(alie));
// L0: A lone signer list cannot be removed.
auto const baseFee = env.current()->fees().base;
env(signers(alice, jtx::none),
msig(bogie),
fee(2 * baseFee),
ter(tecNO_ALTERNATIVE_KEY));
// Enable the master key.
env(fclear(alice, asfDisableMaster), msig(bogie), fee(2 * baseFee));
// L1: The signer list can be removed if the master key is enabled.
env(signers(alice, jtx::none), msig(bogie), fee(2 * baseFee));
// Add a signer list.
env(signers(alice, 1, {{bogie, 1}}), sig(alice));
// M2: The master key can be disabled if there's a signer list.
env(fset(alice, asfDisableMaster), sig(alice));
// Add a regular key.
env(regkey(alice, alie), msig(bogie), fee(2 * baseFee));
// L2: The signer list can be removed if there's a regular key.
env(signers(alice, jtx::none), sig(alie));
// Enable the master key.
env(fclear(alice, asfDisableMaster), sig(alie));
// R2: The regular key can be removed if the master key is enabled.
env(regkey(alice, disabled), sig(alie));
}
// Verify that the first regular key can be made for free using the
// master key, but not when multisigning.
void
test_regKey(FeatureBitset features)
{
testcase("Regular Key");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::secp256k1};
env.fund(XRP(1000), alice);
// Give alice a regular key with a zero fee. Should succeed. Once.
Account const alie{"alie", KeyType::ed25519};
env(regkey(alice, alie), sig(alice), fee(0));
// Try it again and creating the regular key for free should fail.
Account const liss{"liss", KeyType::secp256k1};
env(regkey(alice, liss), sig(alice), fee(0), ter(telINSUF_FEE_P));
// But paying to create a regular key should succeed.
env(regkey(alice, liss), sig(alice));
// In contrast, trying to multisign for a regular key with a zero
// fee should always fail. Even the first time.
Account const becky{"becky", KeyType::ed25519};
env.fund(XRP(1000), becky);
env(signers(becky, 1, {{alice, 1}}), sig(becky));
env(regkey(becky, alie), msig(alice), fee(0), ter(telINSUF_FEE_P));
// Using the master key to sign for a regular key for free should
// still work.
env(regkey(becky, alie), sig(becky), fee(0));
}
// See if every kind of transaction can be successfully multi-signed.
void
test_txTypes(FeatureBitset features)
{
testcase("Transaction Types");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::secp256k1};
Account const becky{"becky", KeyType::ed25519};
Account const zelda{"zelda", KeyType::secp256k1};
Account const gw{"gw"};
auto const USD = gw["USD"];
env.fund(XRP(1000), alice, becky, zelda, gw);
env.close();
// alice uses a regular key with the master disabled.
Account const alie{"alie", KeyType::secp256k1};
env(regkey(alice, alie));
env(fset(alice, asfDisableMaster), sig(alice));
// Attach signers to alice.
env(signers(alice, 2, {{becky, 1}, {bogie, 1}}), sig(alie));
env.close();
int const signerListOwners{features[featureMultiSignReserve] ? 1 : 4};
env.require(owners(alice, signerListOwners + 0));
// Multisign a ttPAYMENT.
auto const baseFee = env.current()->fees().base;
std::uint32_t aliceSeq = env.seq(alice);
env(pay(alice, env.master, XRP(1)),
msig(becky, bogie),
fee(3 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Multisign a ttACCOUNT_SET.
aliceSeq = env.seq(alice);
env(noop(alice), msig(becky, bogie), fee(3 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Multisign a ttREGULAR_KEY_SET.
aliceSeq = env.seq(alice);
Account const ace{"ace", KeyType::secp256k1};
env(regkey(alice, ace), msig(becky, bogie), fee(3 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Multisign a ttTRUST_SET
env(trust("alice", USD(100)),
msig(becky, bogie),
fee(3 * baseFee),
require(lines("alice", 1)));
env.close();
env.require(owners(alice, signerListOwners + 1));
// Multisign a ttOFFER_CREATE transaction.
env(pay(gw, alice, USD(50)));
env.close();
env.require(balance(alice, USD(50)));
env.require(balance(gw, alice["USD"](-50)));
std::uint32_t const offerSeq = env.seq(alice);
env(offer(alice, XRP(50), USD(50)),
msig(becky, bogie),
fee(3 * baseFee));
env.close();
env.require(owners(alice, signerListOwners + 2));
// Now multisign a ttOFFER_CANCEL canceling the offer we just created.
{
aliceSeq = env.seq(alice);
env(offer_cancel(alice, offerSeq),
seq(aliceSeq),
msig(becky, bogie),
fee(3 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
env.require(owners(alice, signerListOwners + 1));
}
// Multisign a ttSIGNER_LIST_SET.
env(signers(alice, 3, {{becky, 1}, {bogie, 1}, {demon, 1}}),
msig(becky, bogie),
fee(3 * baseFee));
env.close();
env.require(owners(alice, features[featureMultiSignReserve] ? 2 : 6));
}
void
test_badSignatureText(FeatureBitset features)
{
testcase("Bad Signature Text");
// Verify that the text returned for signature failures is correct.
using namespace jtx;
Env env{*this, features};
// lambda that submits an STTx and returns the resulting JSON.
auto submitSTTx = [&env](STTx const& stx) {
Json::Value jvResult;
jvResult[jss::tx_blob] = strHex(stx.getSerializer().slice());
return env.rpc("json", "submit", to_string(jvResult));
};
Account const alice{"alice"};
env.fund(XRP(1000), alice);
env(signers(alice, 1, {{bogie, 1}, {demon, 1}}), sig(alice));
auto const baseFee = env.current()->fees().base;
{
// Single-sign, but leave an empty SigningPubKey.
JTx tx = env.jt(noop(alice), sig(alice));
STTx local = *(tx.stx);
local.setFieldVL(sfSigningPubKey, Blob()); // Empty SigningPubKey
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception] ==
"fails local checks: Empty SigningPubKey.");
}
{
// Single-sign, but invalidate the signature.
JTx tx = env.jt(noop(alice), sig(alice));
STTx local = *(tx.stx);
// Flip some bits in the signature.
auto badSig = local.getFieldVL(sfTxnSignature);
badSig[20] ^= 0xAA;
local.setFieldVL(sfTxnSignature, badSig);
// Signature should fail.
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception] ==
"fails local checks: Invalid signature.");
}
{
// Single-sign, but invalidate the sequence number.
JTx tx = env.jt(noop(alice), sig(alice));
STTx local = *(tx.stx);
// Flip some bits in the signature.
auto seq = local.getFieldU32(sfSequence);
local.setFieldU32(sfSequence, seq + 1);
// Signature should fail.
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception] ==
"fails local checks: Invalid signature.");
}
{
// Multisign, but leave a nonempty sfSigningPubKey.
JTx tx = env.jt(noop(alice), fee(2 * baseFee), msig(bogie));
STTx local = *(tx.stx);
local[sfSigningPubKey] = alice.pk(); // Insert sfSigningPubKey
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception] ==
"fails local checks: Cannot both single- and multi-sign.");
}
{
// Both multi- and single-sign with an empty SigningPubKey.
JTx tx = env.jt(noop(alice), fee(2 * baseFee), msig(bogie));
STTx local = *(tx.stx);
local.sign(alice.pk(), alice.sk());
local.setFieldVL(sfSigningPubKey, Blob()); // Empty SigningPubKey
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception] ==
"fails local checks: Cannot both single- and multi-sign.");
}
{
// Multisign but invalidate one of the signatures.
JTx tx = env.jt(noop(alice), fee(2 * baseFee), msig(bogie));
STTx local = *(tx.stx);
// Flip some bits in the signature.
auto& signer = local.peekFieldArray(sfSigners).back();
auto badSig = signer.getFieldVL(sfTxnSignature);
badSig[20] ^= 0xAA;
signer.setFieldVL(sfTxnSignature, badSig);
// Signature should fail.
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception].asString().find(
"Invalid signature on account r") != std::string::npos);
}
{
// Multisign with an empty signers array should fail.
JTx tx = env.jt(noop(alice), fee(2 * baseFee), msig(bogie));
STTx local = *(tx.stx);
local.peekFieldArray(sfSigners).clear(); // Empty Signers array.
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception] ==
"fails local checks: Invalid Signers array size.");
}
{
// Multisign 9 (!ExpandedSignerList) | 33 (ExpandedSignerList) times
// should fail.
JTx tx = env.jt(
noop(alice),
fee(2 * baseFee),
features[featureExpandedSignerList] ? msig(
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie)
: msig(
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie,
bogie));
STTx local = *(tx.stx);
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception] ==
"fails local checks: Invalid Signers array size.");
}
{
// The account owner may not multisign for themselves.
JTx tx = env.jt(noop(alice), fee(2 * baseFee), msig(alice));
STTx local = *(tx.stx);
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception] ==
"fails local checks: Invalid multisigner.");
}
{
// No duplicate multisignatures allowed.
JTx tx = env.jt(noop(alice), fee(2 * baseFee), msig(bogie, bogie));
STTx local = *(tx.stx);
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception] ==
"fails local checks: Duplicate Signers not allowed.");
}
{
// Multisignatures must be submitted in sorted order.
JTx tx = env.jt(noop(alice), fee(2 * baseFee), msig(bogie, demon));
STTx local = *(tx.stx);
// Unsort the Signers array.
auto& signers = local.peekFieldArray(sfSigners);
std::reverse(signers.begin(), signers.end());
// Signature should fail.
auto const info = submitSTTx(local);
BEAST_EXPECT(
info[jss::result][jss::error_exception] ==
"fails local checks: Unsorted Signers array.");
}
}
void
test_noMultiSigners(FeatureBitset features)
{
testcase("No Multisigners");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::ed25519};
Account const becky{"becky", KeyType::secp256k1};
env.fund(XRP(1000), alice, becky);
env.close();
auto const baseFee = env.current()->fees().base;
env(noop(alice),
msig(becky, demon),
fee(3 * baseFee),
ter(tefNOT_MULTI_SIGNING));
}
void
test_multisigningMultisigner(FeatureBitset features)
{
testcase("Multisigning multisigner");
// Set up a signer list where one of the signers has both the
// master disabled and no regular key (because that signer is
// exclusively multisigning). That signer should no longer be
// able to successfully sign the signer list.
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::ed25519};
Account const becky{"becky", KeyType::secp256k1};
env.fund(XRP(1000), alice, becky);
env.close();
// alice sets up a signer list with becky as a signer.
env(signers(alice, 1, {{becky, 1}}));
env.close();
// becky sets up her signer list.
env(signers(becky, 1, {{bogie, 1}, {demon, 1}}));
env.close();
// Because becky has not (yet) disabled her master key, she can
// multisign a transaction for alice.
auto const baseFee = env.current()->fees().base;
env(noop(alice), msig(becky), fee(2 * baseFee));
env.close();
// Now becky disables her master key.
env(fset(becky, asfDisableMaster));
env.close();
// Since becky's master key is disabled she can no longer
// multisign for alice.
env(noop(alice),
msig(becky),
fee(2 * baseFee),
ter(tefMASTER_DISABLED));
env.close();
// Becky cannot 2-level multisign for alice. 2-level multisigning
// is not supported.
env(noop(alice),
msig(msig::Reg{becky, bogie}),
fee(2 * baseFee),
ter(tefBAD_SIGNATURE));
env.close();
// Verify that becky cannot sign with a regular key that she has
// not yet enabled.
Account const beck{"beck", KeyType::ed25519};
env(noop(alice),
msig(msig::Reg{becky, beck}),
fee(2 * baseFee),
ter(tefBAD_SIGNATURE));
env.close();
// Once becky gives herself the regular key, she can sign for alice
// using that regular key.
env(regkey(becky, beck), msig(demon), fee(2 * baseFee));
env.close();
env(noop(alice), msig(msig::Reg{becky, beck}), fee(2 * baseFee));
env.close();
// The presence of becky's regular key does not influence whether she
// can 2-level multisign; it still won't work.
env(noop(alice),
msig(msig::Reg{becky, demon}),
fee(2 * baseFee),
ter(tefBAD_SIGNATURE));
env.close();
}
void
test_signForHash(FeatureBitset features)
{
testcase("sign_for Hash");
// Make sure that the "hash" field returned by the "sign_for" RPC
// command matches the hash returned when that command is sent
// through "submit_multisigned". Make sure that hash also locates
// the transaction in the ledger.
using namespace jtx;
Account const alice{"alice", KeyType::ed25519};
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->loadFromString("[" SECTION_SIGNING_SUPPORT "]\ntrue");
return cfg;
}),
features);
env.fund(XRP(1000), alice);
env.close();
env(signers(alice, 2, {{bogie, 1}, {ghost, 1}}));
env.close();
// Use sign_for to sign a transaction where alice pays 10 XRP to
// masterpassphrase.
auto const baseFee = env.current()->fees().base;
Json::Value jvSig1;
jvSig1[jss::account] = bogie.human();
jvSig1[jss::secret] = bogie.name();
jvSig1[jss::tx_json][jss::Account] = alice.human();
jvSig1[jss::tx_json][jss::Amount] = 10000000;
jvSig1[jss::tx_json][jss::Destination] = env.master.human();
jvSig1[jss::tx_json][jss::Fee] = (3 * baseFee).jsonClipped();
jvSig1[jss::tx_json][jss::Sequence] = env.seq(alice);
jvSig1[jss::tx_json][jss::TransactionType] = jss::Payment;
Json::Value jvSig2 = env.rpc("json", "sign_for", to_string(jvSig1));
BEAST_EXPECT(jvSig2[jss::result][jss::status].asString() == "success");
// Save the hash with one signature for use later.
std::string const hash1 =
jvSig2[jss::result][jss::tx_json][jss::hash].asString();
// Add the next signature and sign again.
jvSig2[jss::result][jss::account] = ghost.human();
jvSig2[jss::result][jss::secret] = ghost.name();
Json::Value jvSubmit =
env.rpc("json", "sign_for", to_string(jvSig2[jss::result]));
BEAST_EXPECT(
jvSubmit[jss::result][jss::status].asString() == "success");
// Save the hash with two signatures for use later.
std::string const hash2 =
jvSubmit[jss::result][jss::tx_json][jss::hash].asString();
BEAST_EXPECT(hash1 != hash2);
// Submit the result of the two signatures.
Json::Value jvResult = env.rpc(
"json", "submit_multisigned", to_string(jvSubmit[jss::result]));
BEAST_EXPECT(
jvResult[jss::result][jss::status].asString() == "success");
BEAST_EXPECT(
jvResult[jss::result][jss::engine_result].asString() ==
"tesSUCCESS");
// The hash from the submit should be the same as the hash from the
// second signing.
BEAST_EXPECT(
hash2 == jvResult[jss::result][jss::tx_json][jss::hash].asString());
env.close();
// The transaction we just submitted should now be available and
// validated.
Json::Value jvTx = env.rpc("tx", hash2);
BEAST_EXPECT(jvTx[jss::result][jss::status].asString() == "success");
BEAST_EXPECT(jvTx[jss::result][jss::validated].asString() == "true");
BEAST_EXPECT(
jvTx[jss::result][jss::meta][sfTransactionResult.jsonName]
.asString() == "tesSUCCESS");
}
void
test_amendmentTransition()
{
testcase("Amendment Transition");
// The OwnerCount associated with a SignerList changes once the
// featureMultiSignReserve amendment goes live. Create a couple
// of signer lists before and after the amendment goes live and
// verify that the OwnerCount is managed properly for all of them.
using namespace jtx;
Account const alice{"alice", KeyType::secp256k1};
Account const becky{"becky", KeyType::ed25519};
Account const cheri{"cheri", KeyType::secp256k1};
Account const daria{"daria", KeyType::ed25519};
Env env{*this, supported_amendments() - featureMultiSignReserve};
env.fund(XRP(1000), alice, becky, cheri, daria);
env.close();
// Give alice and becky signer lists before the amendment goes live.
env(signers(alice, 1, {{bogie, 1}}));
env(signers(
becky,
1,
{{bogie, 1},
{demon, 1},
{ghost, 1},
{haunt, 1},
{jinni, 1},
{phase, 1},
{shade, 1},
{spook, 1}}));
env.close();
env.require(owners(alice, 3));
env.require(owners(becky, 10));
// Enable the amendment.
env.enableFeature(featureMultiSignReserve);
env.close();
// Give cheri and daria signer lists after the amendment goes live.
env(signers(cheri, 1, {{bogie, 1}}));
env(signers(
daria,
1,
{{bogie, 1},
{demon, 1},
{ghost, 1},
{haunt, 1},
{jinni, 1},
{phase, 1},
{shade, 1},
{spook, 1}}));
env.close();
env.require(owners(alice, 3));
env.require(owners(becky, 10));
env.require(owners(cheri, 1));
env.require(owners(daria, 1));
// Delete becky's signer list; her OwnerCount should drop to zero.
// Replace alice's signer list; her OwnerCount should drop to one.
env(signers(becky, jtx::none));
env(signers(
alice,
1,
{{bogie, 1},
{demon, 1},
{ghost, 1},
{haunt, 1},
{jinni, 1},
{phase, 1},
{shade, 1},
{spook, 1}}));
env.close();
env.require(owners(alice, 1));
env.require(owners(becky, 0));
env.require(owners(cheri, 1));
env.require(owners(daria, 1));
// Delete the three remaining signer lists. Everybody's OwnerCount
// should now be zero.
env(signers(alice, jtx::none));
env(signers(cheri, jtx::none));
env(signers(daria, jtx::none));
env.close();
env.require(owners(alice, 0));
env.require(owners(becky, 0));
env.require(owners(cheri, 0));
env.require(owners(daria, 0));
}
void
test_signersWithTickets(FeatureBitset features)
{
testcase("Signers With Tickets");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::ed25519};
env.fund(XRP(2000), alice);
env.close();
// Create a few tickets that alice can use up.
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 20));
env.close();
std::uint32_t const aliceSeq = env.seq(alice);
// Attach phantom signers to alice using a ticket.
env(signers(alice, 1, {{bogie, 1}, {demon, 1}}),
ticket::use(aliceTicketSeq++));
env.close();
env.require(tickets(alice, env.seq(alice) - aliceTicketSeq));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// This should work.
auto const baseFee = env.current()->fees().base;
env(noop(alice),
msig(bogie, demon),
fee(3 * baseFee),
ticket::use(aliceTicketSeq++));
env.close();
env.require(tickets(alice, env.seq(alice) - aliceTicketSeq));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Should also be able to remove the signer list using a ticket.
env(signers(alice, jtx::none), ticket::use(aliceTicketSeq++));
env.close();
env.require(tickets(alice, env.seq(alice) - aliceTicketSeq));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
void
test_signersWithTags(FeatureBitset features)
{
if (!features[featureExpandedSignerList])
return;
testcase("Signers With Tags");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice", KeyType::ed25519};
env.fund(XRP(1000), alice);
env.close();
uint8_t tag1[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint8_t tag2[] =
"hello world some ascii 32b long"; // including 1 byte for NUL
uint256 bogie_tag = ripple::base_uint<256>::fromVoid(tag1);
uint256 demon_tag = ripple::base_uint<256>::fromVoid(tag2);
// Attach phantom signers to alice and use them for a transaction.
env(signers(alice, 1, {{bogie, 1, bogie_tag}, {demon, 1, demon_tag}}));
env.close();
env.require(owners(alice, features[featureMultiSignReserve] ? 1 : 4));
// This should work.
auto const baseFee = env.current()->fees().base;
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice), msig(bogie, demon), fee(3 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Either signer alone should work.
aliceSeq = env.seq(alice);
env(noop(alice), msig(bogie), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
aliceSeq = env.seq(alice);
env(noop(alice), msig(demon), fee(2 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Duplicate signers should fail.
aliceSeq = env.seq(alice);
env(noop(alice), msig(demon, demon), fee(3 * baseFee), ter(temINVALID));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// A non-signer should fail.
aliceSeq = env.seq(alice);
env(noop(alice),
msig(bogie, spook),
fee(3 * baseFee),
ter(tefBAD_SIGNATURE));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Don't meet the quorum. Should fail.
env(signers(alice, 2, {{bogie, 1}, {demon, 1}}));
aliceSeq = env.seq(alice);
env(noop(alice), msig(bogie), fee(2 * baseFee), ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Meet the quorum. Should succeed.
aliceSeq = env.seq(alice);
env(noop(alice), msig(bogie, demon), fee(3 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
void
test_nestedMultiSign(FeatureBitset features)
{
testcase("Nested MultiSign");
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define LINE_TO_HEX_STRING \
[]() -> std::string { \
const char* line = TOSTRING(__LINE__); \
int len = 0; \
while (line[len]) \
len++; \
std::string result; \
if (len % 2 == 1) \
{ \
result += (char)(0x00 * 16 + (line[0] - '0')); \
line++; \
} \
for (int i = 0; line[i]; i += 2) \
{ \
result += (char)((line[i] - '0') * 16 + (line[i + 1] - '0')); \
} \
return result; \
}()
#define M(m) memo(m, "", "")
#define L() memo(LINE_TO_HEX_STRING, "", "")
using namespace jtx;
Env env{*this, envconfig(), features};
// Env env{*this, envconfig(), features, nullptr,
// beast::severities::kTrace};
Account const alice{"alice", KeyType::secp256k1};
Account const becky{"becky", KeyType::ed25519};
Account const cheri{"cheri", KeyType::secp256k1};
Account const daria{"daria", KeyType::ed25519};
Account const edgar{"edgar", KeyType::secp256k1};
Account const fiona{"fiona", KeyType::ed25519};
Account const grace{"grace", KeyType::secp256k1};
Account const henry{"henry", KeyType::ed25519};
Account const f1{"f1", KeyType::ed25519};
Account const f2{"f2", KeyType::ed25519};
Account const f3{"f3", KeyType::ed25519};
env.fund(
XRP(1000),
alice,
becky,
cheri,
daria,
edgar,
fiona,
grace,
henry,
f1,
f2,
f3,
phase,
jinni,
acc10,
acc11,
acc12);
env.close();
auto const baseFee = env.current()->fees().base;
if (!features[featureNestedMultiSign])
{
// When feature is disabled, nested signing should fail
env(signers(f1, 1, {{f2, 1}}));
env(signers(f2, 1, {{f3, 1}}));
env.close();
std::uint32_t f1Seq = env.seq(f1);
env(noop(f1),
msig({msigner(f2, msigner(f3))}),
L(),
fee(3 * baseFee),
ter(temINVALID));
env.close();
BEAST_EXPECT(env.seq(f1) == f1Seq);
return;
}
// Test Case 1: Basic 2-level nested signing with quorum
{
// Set up signer lists with quorum requirements
env(signers(becky, 2, {{bogie, 1}, {demon, 1}, {ghost, 1}}));
env(signers(cheri, 3, {{haunt, 2}, {jinni, 2}}));
env.close();
// Alice requires quorum of 3 with weighted signers
env(signers(alice, 3, {{becky, 2}, {cheri, 2}, {daria, 1}}));
env.close();
// Test 1a: becky alone (weight 2) doesn't meet alice's quorum
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(becky, msigner(bogie), msigner(demon))}),
L(),
fee(4 * baseFee),
ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Test 1b: becky (2) + daria (1) meets quorum of 3
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(becky, msigner(bogie), msigner(demon)),
msigner(daria)}),
L(),
fee(5 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Test 1c: cheri's nested signers must meet her quorum
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(
becky,
msigner(bogie),
msigner(demon)), // becky has a satisfied quorum
msigner(cheri, msigner(haunt))}), // but cheri does not
// (needs jinni too)
L(),
fee(5 * baseFee),
ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Test 1d: cheri with both signers meets her quorum
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(cheri, msigner(haunt), msigner(jinni)),
msigner(daria)}),
L(),
fee(5 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 2: 3-level maximum depth with quorum at each level
{
// Level 2: phase needs direct signatures (no deeper nesting)
env(signers(phase, 2, {{acc10, 1}, {acc11, 1}, {acc12, 1}}));
// Level 1: jinni needs weighted signatures
env(signers(jinni, 3, {{phase, 2}, {shade, 2}, {spook, 1}}));
// Level 0: edgar needs 2 from weighted signers
env(signers(edgar, 2, {{jinni, 1}, {bogie, 1}, {demon, 1}}));
// Alice now requires edgar with weight 3
env(signers(alice, 3, {{edgar, 3}, {fiona, 2}}));
env.close();
// Test 2a: 3-level signing with phase signing directly (not through
// nested signers)
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({
msigner(
edgar,
msigner(
jinni,
msigner(phase), // phase signs directly at level 3
msigner(shade)) // jinni quorum: 2+2 = 4 >= 3 ✓
) // edgar quorum: 1+0 = 1 < 2 ✗
}),
L(),
fee(4 * baseFee),
ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Test 2b: Edgar needs to meet his quorum too
aliceSeq = env.seq(alice);
env(noop(alice),
msig({
msigner(
edgar,
msigner(
jinni,
msigner(phase), // phase signs directly
msigner(shade)),
msigner(bogie)) // edgar quorum: 1+1 = 2 ✓
}),
L(),
fee(5 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Test 2c: Use phase's signers (making it effectively 3-level from
// alice)
aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(
edgar,
msigner(
jinni,
msigner(phase, msigner(acc10), msigner(acc11)),
msigner(spook)),
msigner(bogie))}),
L(),
fee(6 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 3: Mixed levels - some direct, some nested at different
// depths (max 3)
{
// Set up mixed-level signing for alice
// grace has direct signers
env(signers(grace, 2, {{bogie, 1}, {demon, 1}}));
// henry has 2-level signers (henry -> becky -> bogie/demon)
env(signers(henry, 1, {{becky, 1}, {cheri, 1}}));
// edgar can be signed for by bogie
env(signers(edgar, 1, {{bogie, 1}, {shade, 1}}));
// Alice has mix of direct and nested signers at different weights
env(signers(
alice,
5,
{
{daria, 1}, // direct signer
{edgar, 2}, // has 2-level signers
{fiona, 1}, // direct signer
{grace, 2}, // has direct signers
{henry, 2} // has 2-level signers
}));
env.close();
// Test 3a: Mix of all levels meeting quorum exactly
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({
msigner(daria), // weight 1, direct
msigner(edgar, msigner(bogie)), // weight 2, 2-level
msigner(grace, msigner(bogie), msigner(demon)) // weight 2,
// 2-level
}),
L(),
fee(6 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Test 3b: 3-level signing through henry
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(fiona), // weight 1, direct
msigner(
grace, msigner(bogie)), // weight 2, 2-level (partial)
msigner(
henry, // weight 2, 3-level
msigner(becky, msigner(bogie), msigner(demon)))}),
L(),
fee(6 * baseFee),
ter(tefBAD_QUORUM)); // grace didn't meet quorum
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Test 3c: Correct version with all quorums met
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(fiona), // weight 1
msigner(
edgar, msigner(bogie), msigner(shade)), // weight 2
msigner(
henry, // weight 2
msigner(becky, msigner(bogie), msigner(demon)))}),
L(),
fee(8 * baseFee)); // Total weight: 1+2+2 = 5 ✓
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 4: Complex scenario with maximum signers at mixed depths
// (max 3)
{
// Create a signing tree that uses close to maximum signers
// and tests weight accumulation across all levels
// Set up for alice: needs 15 out of possible 20 weight
env(signers(
alice,
15,
{
{becky, 3}, // will use 2-level
{cheri, 3}, // will use 2-level
{daria, 3}, // will use direct
{edgar, 3}, // will use 2-level
{fiona, 3}, // will use direct
{grace, 3}, // will use direct
{henry, 2} // will use 2-level
}));
env.close();
// Complex multi-level transaction just meeting quorum
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({
msigner(
becky, // weight 3, 2-level
msigner(demon),
msigner(ghost)),
msigner(
cheri, // weight 3, 2-level
msigner(haunt),
msigner(jinni)),
msigner(daria), // weight 3, direct
msigner(
edgar, // weight 3, 2-level
msigner(bogie)),
msigner(grace) // weight 3, direct
}),
L(),
fee(10 * baseFee)); // Total weight: 3+3+3+3+3 = 15 ✓
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Test 4b: Test with henry using 3-level depth (maximum)
// First set up henry's chain properly
env(signers(henry, 1, {{jinni, 1}}));
env(signers(jinni, 2, {{acc10, 1}, {acc11, 1}}));
env.close();
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(
becky, // weight 3
msigner(demon)), // becky quorum not met!
msigner(
cheri, // weight 3
msigner(haunt),
msigner(jinni)),
msigner(daria), // weight 3
msigner(
henry, // weight 2, 3-level depth
msigner(jinni, msigner(acc10), msigner(acc11))),
msigner(
edgar, // weight 3
msigner(bogie),
msigner(shade))}),
L(),
fee(10 * baseFee),
ter(tefBAD_QUORUM)); // becky's quorum not met
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
// Test Case 5: Edge case - single signer with maximum nesting (depth 3)
{
// Alice needs just one signer, but that signer uses depth up to 3
env(signers(alice, 1, {{becky, 1}}));
env.close();
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(becky, msigner(demon), msigner(ghost))}),
L(),
fee(4 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Now with 3-level depth (maximum allowed)
// Structure: alice -> becky -> cheri -> jinni (jinni signs
// directly)
env(signers(becky, 1, {{cheri, 1}}));
env(signers(cheri, 1, {{jinni, 1}}));
// Note: We do NOT add signers to jinni to keep max depth at 3
env.close();
aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(
becky,
msigner(
cheri,
msigner(jinni)))}), // jinni signs directly (depth 3)
L(),
fee(4 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 6: Simple cycle detection (A -> B -> A)
{
testcase("Cycle Detection - Simple");
// Reset signer lists for clean state
env(signers(alice, jtx::none));
env(signers(becky, jtx::none));
env.close();
// becky's signer list includes alice
// alice's signer list includes becky
// This creates: alice -> becky -> alice (cycle)
env(signers(alice, 1, {{becky, 1}, {bogie, 1}}));
env(signers(becky, 1, {{alice, 1}, {demon, 1}}));
env.close();
// Without cycle relaxation this would fail because:
// - alice needs becky (weight 1)
// - becky needs alice, but alice is ancestor -> cycle
// - becky's effective quorum relaxes since alice is unavailable
// - demon can satisfy becky's relaxed quorum
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(becky, msigner(demon))}),
L(),
fee(4 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Test that direct signer still works normally
aliceSeq = env.seq(alice);
env(noop(alice), msig({msigner(bogie)}), L(), fee(3 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 7: The specific lockout scenario
// onyx:{jade, nova:{ruby:{jade, nova}, jade}}
// All have quorum 2, only jade can actually sign
{
testcase("Cycle Detection - Complex Lockout");
Account const onyx{"onyx", KeyType::secp256k1};
Account const nova{"nova", KeyType::ed25519};
Account const ruby{"ruby", KeyType::secp256k1};
Account const jade{"jade", KeyType::ed25519}; // phantom signer
env.fund(XRP(1000), onyx, nova, ruby);
env.close();
// Set up signer lists FIRST (before disabling master keys)
// ruby: {jade, nova} with quorum 2
env(signers(ruby, 2, {{jade, 1}, {nova, 1}}));
// nova: {ruby, jade} with quorum 2
env(signers(nova, 2, {{jade, 1}, {ruby, 1}}));
// onyx: {jade, nova} with quorum 2
env(signers(onyx, 2, {{jade, 1}, {nova, 1}}));
env.close();
// NOW disable master keys (signer lists provide alternative)
env(fset(onyx, asfDisableMaster), sig(onyx));
env(fset(nova, asfDisableMaster), sig(nova));
env(fset(ruby, asfDisableMaster), sig(ruby));
env.close();
// The signing tree for onyx:
// onyx (quorum 2) -> jade (weight 1) + nova (weight 1)
// nova (quorum 2) -> jade (weight 1) + ruby (weight 1)
// ruby (quorum 2) -> jade (weight 1) + nova (weight 1, CYCLE!)
//
// Without cycle detection: ruby needs nova, but nova is ancestor ->
// stuck With cycle detection:
// - At ruby level: nova is cyclic, cyclicWeight=1, totalWeight=2
// - maxAchievable = 2-1 = 1 < quorum(2), so effectiveQuorum -> 1
// - jade alone can satisfy ruby's relaxed quorum
// - ruby satisfied -> nova gets ruby's weight
// - nova: jade(1) + ruby(1) = 2 >= quorum(2) ✓
// - onyx: jade(1) + nova(1) = 2 >= quorum(2) ✓
std::uint32_t onyxSeq = env.seq(onyx);
env(noop(onyx),
msig(
{msigner(jade),
msigner(
nova,
msigner(jade),
msigner(
ruby, msigner(jade)))}), // nova is cyclic,
// skipped at ruby level
L(),
fee(6 * baseFee));
env.close();
BEAST_EXPECT(env.seq(onyx) == onyxSeq + 1);
}
// Test Case 8: Cycle where all signers are cyclic (effectiveQuorum ==
// 0)
{
testcase("Cycle Detection - Total Lockout");
Account const alpha{"alpha", KeyType::secp256k1};
Account const beta{"beta", KeyType::ed25519};
Account const gamma{"gamma", KeyType::secp256k1};
env.fund(XRP(1000), alpha, beta, gamma);
env.close();
// Set up pure cycle signer lists FIRST
env(signers(alpha, 1, {{beta, 1}}));
env(signers(beta, 1, {{gamma, 1}}));
env(signers(gamma, 1, {{alpha, 1}}));
env.close();
// NOW disable master keys
env(fset(alpha, asfDisableMaster), sig(alpha));
env(fset(beta, asfDisableMaster), sig(beta));
env(fset(gamma, asfDisableMaster), sig(gamma));
env.close();
// This is a true lockout - no valid signing path exists.
// gamma appears as a leaf signer but has master disabled ->
// tefMASTER_DISABLED (The cycle detection would return
// tefBAD_QUORUM if gamma were nested, but there's no way to
// construct such a transaction since gamma's only signer is alpha,
// which is what we're trying to sign for)
std::uint32_t alphaSeq = env.seq(alpha);
env(noop(alpha),
msig({msigner(
beta,
msigner(gamma))}), // gamma can't sign - master disabled
L(),
fee(4 * baseFee),
ter(tefMASTER_DISABLED));
env.close();
BEAST_EXPECT(env.seq(alpha) == alphaSeq);
}
// Test Case 9: Cycle at depth 3 (near max depth)
{
testcase("Cycle Detection - Deep Cycle");
// Reset signer lists
env(signers(alice, jtx::none));
env(signers(becky, jtx::none));
env(signers(cheri, jtx::none));
env(signers(daria, jtx::none));
env.close();
// Structure: alice -> becky -> cheri -> daria -> alice (cycle at
// depth 4)
env(signers(alice, 1, {{becky, 1}, {bogie, 1}}));
env(signers(becky, 1, {{cheri, 1}}));
env(signers(cheri, 1, {{daria, 1}}));
env(signers(daria, 1, {{alice, 1}, {demon, 1}}));
env.close();
// At depth 4, daria needs alice but alice is ancestor
// daria's quorum relaxes, demon can satisfy
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(
becky, msigner(cheri, msigner(daria, msigner(demon))))}),
L(),
fee(6 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 10: Multiple independent cycles in same tree
{
testcase("Cycle Detection - Multiple Cycles");
// Reset signer lists
env(signers(alice, jtx::none));
env(signers(becky, jtx::none));
env(signers(cheri, jtx::none));
env.close();
// alice -> {becky, cheri}
// becky -> {alice, bogie} (cycle back to alice)
// cheri -> {alice, demon} (another cycle back to alice)
env(signers(alice, 2, {{becky, 1}, {cheri, 1}}));
env(signers(becky, 2, {{alice, 1}, {bogie, 1}}));
env(signers(cheri, 2, {{alice, 1}, {demon, 1}}));
env.close();
// Both becky and cheri have cycles back to alice
// Both need their quorums relaxed
// bogie satisfies becky, demon satisfies cheri
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(becky, msigner(bogie)),
msigner(cheri, msigner(demon))}),
L(),
fee(6 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 11: Cycle with sufficient non-cyclic weight (no relaxation
// needed)
{
testcase("Cycle Detection - No Relaxation Needed");
// Reset signer lists
env(signers(alice, jtx::none));
env(signers(becky, jtx::none));
env.close();
// becky has alice in signer list but also has enough other signers
env(signers(alice, 1, {{becky, 1}}));
env(signers(becky, 2, {{alice, 1}, {bogie, 1}, {demon, 1}}));
env.close();
// becky quorum is 2, alice is cyclic (weight 1)
// totalWeight = 3, cyclicWeight = 1, maxAchievable = 2 >= quorum
// No relaxation needed, bogie + demon satisfy quorum normally
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(becky, msigner(bogie), msigner(demon))}),
L(),
fee(5 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Should fail if only one non-cyclic signer provided
aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(becky, msigner(bogie))}),
L(),
fee(4 * baseFee),
ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
// Test Case 12: Partial cycle - one branch cyclic, one not
{
testcase("Cycle Detection - Partial Cycle");
// Reset signer lists
env(signers(alice, jtx::none));
env(signers(becky, jtx::none));
env(signers(cheri, jtx::none));
env.close();
// alice -> {becky, cheri}
// becky -> {alice, bogie} (cyclic)
// cheri -> {daria} (not cyclic)
env(signers(alice, 2, {{becky, 1}, {cheri, 1}}));
env(signers(becky, 1, {{alice, 1}, {bogie, 1}}));
env(signers(cheri, 1, {{daria, 1}}));
env.close();
// becky's branch has cycle, cheri's doesn't
// Both contribute to alice's quorum
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(becky, msigner(bogie)), // relaxed quorum
msigner(cheri, msigner(daria))}), // normal quorum
L(),
fee(6 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 13: Diamond pattern with cycle
{
testcase("Cycle Detection - Diamond Pattern");
// Reset signer lists
env(signers(alice, jtx::none));
env(signers(becky, jtx::none));
env(signers(cheri, jtx::none));
env(signers(daria, jtx::none));
env.close();
// alice -> {becky, cheri}
// becky -> {daria}
// cheri -> {daria}
// daria -> {alice, bogie} (cycle through both paths)
env(signers(alice, 2, {{becky, 1}, {cheri, 1}}));
env(signers(becky, 1, {{daria, 1}}));
env(signers(cheri, 1, {{daria, 1}}));
env(signers(daria, 1, {{alice, 1}, {bogie, 1}}));
env.close();
// Both paths converge at daria, which cycles back to alice
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(becky, msigner(daria, msigner(bogie))),
msigner(cheri, msigner(daria, msigner(bogie)))}),
L(),
fee(7 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 14: Cycle requiring maximum quorum relaxation
{
testcase("Cycle Detection - Maximum Relaxation");
Account const omega{"omega", KeyType::secp256k1};
Account const sigma{"sigma", KeyType::ed25519};
env.fund(XRP(1000), omega, sigma);
env.close();
// Reset alice and becky signer lists
env(signers(alice, jtx::none));
env(signers(becky, jtx::none));
env.close();
// Set up signer lists FIRST
env(signers(sigma, 1, {{omega, 1}, {bogie, 1}}));
env(signers(omega, 3, {{sigma, 2}, {alice, 1}, {becky, 1}}));
env(signers(alice, 1, {{omega, 1}, {demon, 1}}));
env(signers(becky, 1, {{omega, 1}, {ghost, 1}}));
env.close();
// NOW disable master keys
env(fset(omega, asfDisableMaster), sig(omega));
env(fset(sigma, asfDisableMaster), sig(sigma));
env.close();
// From omega's perspective when signing for omega:
// - sigma: needs omega (cyclic), so relaxes to bogie only
// - alice: needs omega (cyclic), so relaxes to demon only
// - becky: needs omega (cyclic), so relaxes to ghost only
// All signers need relaxation but can be satisfied
std::uint32_t omegaSeq = env.seq(omega);
env(noop(omega),
msig(
{msigner(alice, msigner(demon)),
msigner(becky, msigner(ghost)),
msigner(sigma, msigner(bogie))}),
L(),
fee(7 * baseFee));
env.close();
BEAST_EXPECT(env.seq(omega) == omegaSeq + 1);
}
// Test Case 15: Cycle at exact max depth boundary
{
testcase("Cycle Detection - Max Depth Boundary");
// Reset signer lists
env(signers(alice, jtx::none));
env(signers(becky, jtx::none));
env(signers(cheri, jtx::none));
env(signers(daria, jtx::none));
env(signers(edgar, jtx::none));
env.close();
// Depth 4 is max: alice(1) -> becky(2) -> cheri(3) -> daria(4)
// daria cycles back but we're at max depth
env(signers(alice, 1, {{becky, 1}}));
env(signers(becky, 1, {{cheri, 1}}));
env(signers(cheri, 1, {{daria, 1}}));
env(signers(daria, 1, {{alice, 1}, {bogie, 1}}));
env.close();
// This should work - cycle detected and relaxed at depth 4
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(
becky, msigner(cheri, msigner(daria, msigner(bogie))))}),
L(),
fee(6 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Now try to exceed depth (add edgar at depth 5)
env(signers(daria, 1, {{edgar, 1}}));
env(signers(edgar, 1, {{bogie, 1}}));
env.close();
// Transaction structure is rejected at preflight for exceeding
// nesting limits
aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(
becky,
msigner(
cheri,
msigner(daria, msigner(edgar, msigner(bogie)))))}),
L(),
fee(7 * baseFee),
ter(temMALFORMED)); // Rejected at preflight for excessive
// nesting
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
}
void
test_signerListSetFlags(FeatureBitset features)
{
using namespace test::jtx;
for (bool const withFixInvalidTxFlags : {false, true})
{
Env env{
*this,
withFixInvalidTxFlags ? features
: features - fixInvalidTxFlags};
Account const alice{"alice"};
env.fund(XRP(1000), alice);
env.close();
testcase(
std::string("SignerListSet flag, fix ") +
(withFixInvalidTxFlags ? "enabled" : "disabled"));
ter const expected(
withFixInvalidTxFlags ? TER(temINVALID_FLAG) : TER(tesSUCCESS));
env(signers(alice, 2, {{bogie, 1}, {ghost, 1}}),
expected,
txflags(tfPassive));
env.close();
}
}
void
testAll(FeatureBitset features)
{
test_noReserve(features);
test_signerListSet(features);
test_phantomSigners(features);
test_fee(features);
test_misorderedSigners(features);
test_masterSigners(features);
test_regularSigners(features);
test_regularSignersUsingSubmitMulti(features);
test_heterogeneousSigners(features);
test_keyDisable(features);
test_regKey(features);
test_txTypes(features);
test_badSignatureText(features);
test_noMultiSigners(features);
test_multisigningMultisigner(features);
test_signForHash(features);
test_signersWithTickets(features);
test_signersWithTags(features);
test_nestedMultiSign(features);
}
void
run() override
{
using namespace jtx;
auto const all = supported_amendments();
// The reserve required on a signer list changes based on
// featureMultiSignReserve. Limits on the number of signers
// changes based on featureExpandedSignerList. Test both with and
// without.
testAll(
all - featureMultiSignReserve - featureExpandedSignerList -
featureNestedMultiSign);
testAll(all - featureExpandedSignerList - featureNestedMultiSign);
testAll(all - featureNestedMultiSign);
testAll(all);
test_signerListSetFlags(all);
test_amendmentTransition();
}
};
BEAST_DEFINE_TESTSUITE(MultiSign, app, ripple);
} // namespace test
} // namespace ripple