#include #include #include #include namespace xrpl { 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 testNoReserve(FeatureBitset features) { testcase("No Reserve"); using namespace jtx; Env env{*this, features}; Account const alice{"alice", KeyType::secp256k1}; // 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; env.fund(XRP(250) - drops(1), alice); env.close(); env.require(owners(alice, 0)); { // Attach a signer list to alice. Should fail. Json::Value signersList = signers(alice, 1, {{bogie, 1}}); env(signersList, 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(signersList); env.close(); env.require(owners(alice, 1)); } { // Pay alice enough to almost make the reserve for the biggest // possible list. env(pay(env.master, alice, 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, 1)); // 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, 1)); } // Remove alice's signer list and get the owner count back. env(signers(alice, jtx::none)); env.close(); env.require(owners(alice, 0)); } void testSignerListSet(FeatureBitset features) { testcase("SignerListSet"); using namespace jtx; Env env{*this, features}; Account const alice{"alice", KeyType::ed25519}; env.fund(XRP(1000), alice); env.close(); // 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)); // clang-format off // Make a signer list that's too big. Should fail. Account const spare("spare", KeyType::secp256k1); env(signers( alice, 1, std::vector{{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}}), ter(temMALFORMED)); // clang-format on env.close(); env.require(owners(alice, 0)); } void testPhantomSigners(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, 1)); // 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), rpc("invalidTransaction", "fails local checks: Duplicate Signers not allowed.")); 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 testFee(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, 1)); // 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 testMisorderedSigners(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, 1)); msig phantoms{bogie, demon}; std::reverse(phantoms.signers.begin(), phantoms.signers.end()); std::uint32_t const aliceSeq = env.seq(alice); env(noop(alice), phantoms, rpc("invalidTransaction", "fails local checks: Unsorted Signers array.")); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq); } void testMasterSigners(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, 1)); // 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 testRegularSigners(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(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(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(Reg{becky, beck}, Reg{cheri, cher})); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); } void testRegularSignersUsingSubmitMulti(FeatureBitset features) { testcase("Regular Signers Using submit_multisigned"); using namespace jtx; Env env( *this, envconfig([](std::unique_ptr 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 testHeterogeneousSigners(FeatureBitset features) { testcase("Heterogeneous 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, 1)); // 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(Reg{cheri, cher}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); env(noop(alice), 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, Reg{cheri, cher}, 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, 1)); aliceSeq = env.seq(alice); env(noop(alice), fee(9 * baseFee), msig(becky, Reg{cheri, cher}, 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, 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, 1)); aliceSeq = env.seq(alice); env(noop(alice), fee(9 * baseFee), msig( becky, Reg{cheri, cher}, 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 testKeyDisable(FeatureBitset features) { testcase("Key Disable"); using namespace jtx; Env env{*this, features}; Account const alice{"alice", KeyType::ed25519}; env.fund(XRP(1000), alice); env.close(); // 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 testRegKey(FeatureBitset features) { testcase("Regular Key"); using namespace jtx; Env env{*this, features}; Account const alice{"alice", KeyType::secp256k1}; env.fund(XRP(1000), alice); env.close(); // 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.close(); 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 testTxTypes(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(); env.require(owners(alice, 1)); // 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, 2)); // 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, 3)); // 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, 2)); } // 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, 2)); } void testBadSignatureText(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.close(); 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."); } { JTx tx = env.jt( noop(alice), fee(2 * baseFee), 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)); 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 testNoMultiSigners(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 testMultisigningMultisigner(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(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(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(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(Reg{becky, demon}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); } void testSignForHash(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 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 testSignersWithTickets(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 testSignersWithTags(FeatureBitset features) { 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 = xrpl::base_uint<256>::fromVoid(tag1); uint256 demon_tag = xrpl::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, 1)); // 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), rpc("invalidTransaction", "fails local checks: Duplicate Signers not allowed.")); 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 testSignerListSetFlags(FeatureBitset features) { using namespace test::jtx; Env env{*this, features}; Account const alice{"alice"}; env.fund(XRP(1000), alice); env.close(); bool const enabled = features[fixInvalidTxFlags]; testcase( std::string("SignerListSet flag, fix ") + (enabled ? "enabled" : "disabled")); ter const expected(enabled ? TER(temINVALID_FLAG) : TER(tesSUCCESS)); env(signers(alice, 2, {{bogie, 1}, {ghost, 1}}), expected, txflags(tfPassive)); env.close(); } void testSignerListObject(FeatureBitset features) { testcase("SignerList Object"); // Verify that the SignerList object is created correctly. 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. env(signers(alice, 1, {{bogie, 1}, {demon, 1}})); env.close(); // Verify that the SignerList object was created correctly. auto const& sle = env.le(keylet::signers(alice.id())); BEAST_EXPECT(sle); BEAST_EXPECT(sle->getFieldArray(sfSignerEntries).size() == 2); if (features[fixIncludeKeyletFields]) { BEAST_EXPECT((*sle)[sfOwner] == alice.id()); } else { BEAST_EXPECT(!sle->isFieldPresent(sfOwner)); } } void testAll(FeatureBitset features) { testNoReserve(features); testSignerListSet(features); testPhantomSigners(features); testFee(features); testMisorderedSigners(features); testMasterSigners(features); testRegularSigners(features); testRegularSignersUsingSubmitMulti(features); testHeterogeneousSigners(features); testKeyDisable(features); testRegKey(features); testTxTypes(features); testBadSignatureText(features); testNoMultiSigners(features); testMultisigningMultisigner(features); testSignForHash(features); testSignersWithTickets(features); testSignersWithTags(features); } void run() override { using namespace jtx; auto const all = testable_amendments(); testAll(all); testSignerListSetFlags(all - fixInvalidTxFlags); testSignerListSetFlags(all); testSignerListObject(all - fixIncludeKeyletFields); testSignerListObject(all); } }; BEAST_DEFINE_TESTSUITE(MultiSign, app, xrpl); } // namespace test } // namespace xrpl