diff --git a/Builds/VisualStudio2013/RippleD.vcxproj b/Builds/VisualStudio2013/RippleD.vcxproj index 493e02346..55c30d7b2 100644 --- a/Builds/VisualStudio2013/RippleD.vcxproj +++ b/Builds/VisualStudio2013/RippleD.vcxproj @@ -2046,6 +2046,20 @@ + + True + True + ..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories) + ..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories) + + + + + True + True + ..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories) + ..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories) + True True @@ -3217,6 +3231,9 @@ True + + True + True diff --git a/Builds/VisualStudio2013/RippleD.vcxproj.filters b/Builds/VisualStudio2013/RippleD.vcxproj.filters index 780471755..a5a1dac66 100644 --- a/Builds/VisualStudio2013/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2013/RippleD.vcxproj.filters @@ -2574,6 +2574,15 @@ ripple\app\tx + + ripple\app\tx\tests + + + ripple\app\tx\tests + + + ripple\app\tx\tests + ripple\app\tx\tests @@ -3747,6 +3756,9 @@ ripple\rpc\handlers + + ripple\rpc\handlers + ripple\rpc\handlers diff --git a/src/ripple/app/ledger/LedgerEntrySet.cpp b/src/ripple/app/ledger/LedgerEntrySet.cpp index f58cda028..e3fc0fcdd 100644 --- a/src/ripple/app/ledger/LedgerEntrySet.cpp +++ b/src/ripple/app/ledger/LedgerEntrySet.cpp @@ -1028,7 +1028,7 @@ void LedgerEntrySet::incrementOwnerCount (Account const& owner) } void -LedgerEntrySet::increaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch) +LedgerEntrySet::increaseOwnerCount (SLE::ref sleAccount, std::uint32_t howMuch) { assert (sleAccount); @@ -1059,7 +1059,7 @@ void LedgerEntrySet::decrementOwnerCount (Account const& owner) } void -LedgerEntrySet::decreaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch) +LedgerEntrySet::decreaseOwnerCount (SLE::ref sleAccount, std::uint32_t howMuch) { assert (sleAccount); diff --git a/src/ripple/app/ledger/LedgerEntrySet.h b/src/ripple/app/ledger/LedgerEntrySet.h index cdc30082b..27b57aa07 100644 --- a/src/ripple/app/ledger/LedgerEntrySet.h +++ b/src/ripple/app/ledger/LedgerEntrySet.h @@ -185,13 +185,13 @@ public: /** @{ */ void incrementOwnerCount (SLE::ref sleAccount); void incrementOwnerCount (Account const& owner); - void increaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch); + void increaseOwnerCount (SLE::ref sleAccount, std::uint32_t howMuch); /** @} */ /** @{ */ void decrementOwnerCount (SLE::ref sleAccount); void decrementOwnerCount (Account const& owner); - void decreaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch); + void decreaseOwnerCount (SLE::ref sleAccount, std::uint32_t howMuch); /** @} */ // Offer functions. diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index e5028af5a..136f01b83 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -143,6 +143,9 @@ void printHelp (const po::options_description& desc) #endif // RIPPLE_ENABLE_MULTI_SIGN " stop\n" " submit\n" +#if RIPPLE_ENABLE_MULTI_SIGN + " submit_multisigned\n" +#endif // RIPPLE_ENABLE_MULTI_SIGN " tx \n" " unl_add | []\n" " unl_delete |\n" diff --git a/src/ripple/app/tx/README.md b/src/ripple/app/tx/README.md index 568feabeb..7d9dd5209 100644 --- a/src/ripple/app/tx/README.md +++ b/src/ripple/app/tx/README.md @@ -29,8 +29,6 @@ classes the ability to replace portions of the default implementation. ## SetSignerList ## -**Associated JIRA task is [RIPD-182](https://ripplelabs.atlassian.net/browse/RIPD-182)** - In order to enhance the flexibility of Ripple and provide support for enhanced security of accounts, native support for "multi-signature" or "multi-sign" accounts is required. @@ -158,8 +156,6 @@ The data for a transaction that removes any signer list has this form: ## Tickets ## -**Associated JIRA task is [RIPD-368](https://ripplelabs.atlassian.net/browse/RIPD-368)** - Currently transactions on the Ripple network require the use of sequence numbers and sequence numbers must monotonically increase. Since the sequence number is part of the transaction, it is "covered" by the signature that diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 6503c81da..17507ba87 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -54,7 +54,7 @@ public: return temUNKNOWN; } - TER checkSig () override + TER checkSign () override { if (mTxn.getFieldAccount160 (sfAccount).isNonZero ()) { diff --git a/src/ripple/app/tx/impl/SetAccount.cpp b/src/ripple/app/tx/impl/SetAccount.cpp index 074dd4e0c..725faf6bd 100644 --- a/src/ripple/app/tx/impl/SetAccount.cpp +++ b/src/ripple/app/tx/impl/SetAccount.cpp @@ -193,7 +193,7 @@ public: { if (!mSigMaster) { - m_journal.trace << "Can't use regular key to disable master key."; + m_journal.trace << "Must use master key to disable master key."; return tecNEED_MASTER_KEY; } diff --git a/src/ripple/app/tx/impl/SetSignerList.cpp b/src/ripple/app/tx/impl/SetSignerList.cpp index 9d893ea60..9e45a4c54 100644 --- a/src/ripple/app/tx/impl/SetSignerList.cpp +++ b/src/ripple/app/tx/impl/SetSignerList.cpp @@ -221,8 +221,8 @@ SetSignerList::replaceSignerList (uint256 const& index) return ter; // Compute new reserve. Verify the account has funds to meet the reserve. - std::size_t const oldOwnerCount = mTxnAccount->getFieldU32 (sfOwnerCount); - std::size_t const addedOwnerCount = ownerCountDelta (signers_.size ()); + std::uint32_t const oldOwnerCount = mTxnAccount->getFieldU32 (sfOwnerCount); + std::uint32_t const addedOwnerCount = ownerCountDelta (signers_.size ()); std::uint64_t const newReserve = mEngine->getLedger ()->getReserve (oldOwnerCount + addedOwnerCount); @@ -277,7 +277,7 @@ SetSignerList::destroySignerList (uint256 const& index) // We have to examine the current SignerList so we know how much to // reduce the OwnerCount. - std::size_t removeFromOwnerCount = 0; + std::uint32_t removeFromOwnerCount = 0; uint256 const signerListIndex = getSignerListIndex (mTxnAccountID); SLE::pointer accountSignersList = mEngine->view ().entryCache (ltSIGNER_LIST, signerListIndex); diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 805de75c9..409965913 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -157,35 +158,6 @@ TER Transactor::payFee () return tesSUCCESS; } -TER Transactor::checkSig () -{ - // Consistency: Check signature and verify the transaction's signing public - // key is the key authorized for signing. - - auto const signing_account = mSigningPubKey.getAccountID (); - - if (signing_account == mTxnAccountID) - { - if (mTxnAccount->isFlag(lsfDisableMaster)) - return tefMASTER_DISABLED; - - mSigMaster = true; - return tesSUCCESS; - } - - if (!mHasAuthKey) - { - m_journal.trace << "Invalid: Not authorized to use account."; - return temBAD_AUTH_MASTER; - } - - if (signing_account == mTxnAccount->getFieldAccount160 (sfRegularKey)) - return tesSUCCESS; - - m_journal.trace << "Delay: Not authorized to use account."; - return tefBAD_AUTH; -} - TER Transactor::checkSeq () { std::uint32_t const t_seq = mTxn.getSequence (); @@ -315,7 +287,7 @@ TER Transactor::apply () if (terResult != tesSUCCESS) return (terResult); - terResult = checkSig (); + terResult = checkSign (); if (terResult != tesSUCCESS) return (terResult); @@ -325,4 +297,364 @@ TER Transactor::apply () return doApply (); } +TER Transactor::checkSign () +{ +#if RIPPLE_ENABLE_MULTI_SIGN + // If the mSigningPubKey is empty, then we must be multi-signing. + TER const signingTER = mSigningPubKey.getAccountPublic ().empty () ? + checkMultiSign () : checkSingleSign (); +#else + TER const signingTER = checkSingleSign (); +#endif // RIPPLE_ENABLE_MULTI_SIGN + + return signingTER; +} + +TER Transactor::checkSingleSign () +{ + // Consistency: Check signature + // Verify the transaction's signing public key is authorized for signing. + if (mSigningPubKey.getAccountID () == mTxnAccountID) + { + // Authorized to continue. + mSigMaster = true; + if (mTxnAccount->isFlag(lsfDisableMaster)) + return tefMASTER_DISABLED; + } + else if (mHasAuthKey && + (mSigningPubKey.getAccountID () == + mTxnAccount->getFieldAccount160 (sfRegularKey))) + { + // Authorized to continue. + } + else if (mHasAuthKey) + { + m_journal.trace << + "applyTransaction: Delay: Not authorized to use account."; + return tefBAD_AUTH; + } + else + { + m_journal.trace << + "applyTransaction: Invalid: Not authorized to use account."; + return tefBAD_AUTH_MASTER; + } + + return tesSUCCESS; +} + +namespace TransactorDetail +{ + +struct GetSignerListResult +{ + TER ter = tefFAILURE; + std::uint32_t quorum = std::numeric_limits ::max (); + std::vector signerEntries; +}; + +// We need the SignerList for every SigningFor while multi-signing. +GetSignerListResult +getSignerList ( + Account signingForAcctID, TransactionEngine* engine, beast::Journal journal) +{ + GetSignerListResult ret; + + uint256 const index = getSignerListIndex (signingForAcctID); + SLE::pointer accountSignersList = + engine->view ().entryCache (ltSIGNER_LIST, index); + + // If the signer list doesn't exist the account is not multi-signing. + if (!accountSignersList) + { + journal.trace << + "applyTransaction: Invalid: Not a multi-signing account."; + ret.ter = tefNOT_MULTI_SIGNING; + return ret; + } + ret.quorum = accountSignersList->getFieldU32 (sfSignerQuorum); + + SignerEntries::Decoded signersOnAccountDecode = + SignerEntries::deserialize (*accountSignersList, journal, "ledger"); + ret.ter = signersOnAccountDecode.ter; + + if (signersOnAccountDecode.ter == tesSUCCESS) + { + ret.signerEntries = std::move (signersOnAccountDecode.vec); + } + return ret; +} + +struct CheckSigningAccountsResult +{ + TER ter = tefFAILURE; + std::uint32_t weightSum = 0; +}; + +// Verify that every SigningAccount is a valid SignerEntry. Sum signer weights. +CheckSigningAccountsResult +checkSigningAccounts ( + std::vector signerEntries, + STArray const& signingAccounts, + TransactionEngine* engine, + beast::Journal journal) +{ + CheckSigningAccountsResult ret; + + // Both the signerEntries and signingAccounts are sorted by account. So + // matching signerEntries to signingAccounts should be a simple + // linear walk. *All* signers must be valid or the transaction fails. + std::uint32_t weightSum = 0; + auto signerEntriesItr = signerEntries.begin (); + for (auto const& signingAccount : signingAccounts) + { + Account const signingAcctID = + signingAccount.getFieldAccount (sfAccount).getAccountID (); + + // Attempt to match the SignerEntry with a SigningAccount; + while (signerEntriesItr->account < signingAcctID) + { + if (++signerEntriesItr == signerEntries.end ()) + { + journal.trace << + "applyTransaction: Invalid SigningAccount.Account."; + ret.ter = tefBAD_SIGNATURE; + return ret; + } + } + if (signerEntriesItr->account != signingAcctID) + { + // The SigningAccount is not in the SignerEntries. + journal.trace << + "applyTransaction: Invalid SigningAccount.Account."; + ret.ter = tefBAD_SIGNATURE; + return ret; + } + if (signerEntriesItr->weight <= 0) + { + // The SigningAccount has a weight of zero and may not sign. + journal.trace << + "applyTransaction: SigningAccount.Account needs weight > 0."; + ret.ter = tefBAD_SIGNATURE; + return ret; + } + + // We found the SigningAccount in the list of valid signers. Now we + // need to compute the accountID that is associated with the signer's + // public key. + Account const signingAcctIDFromPubKey = + RippleAddress::createAccountPublic ( + signingAccount.getFieldVL (sfSigningPubKey)).getAccountID (); + + // Verify that the signingAcctID and the signingAcctIDFromPubKey + // belong together. Here is are the rules: + // + // 1. "Phantom account": an account that is not in the ledger + // A. If signingAcctID == signingAcctIDFromPubKey and the + // signingAcctID is not in the ledger then we have a phantom + // account. + // B. Phantom accounts are always allowed as multi-signers. + // + // 2. "Master Key" + // A. signingAcctID == signingAcctIDFromPubKey, and signingAcctID + // is in the ledger. + // B. If the signingAcctID in the ledger does not have the + // asfDisableMaster flag set, then the signature is allowed. + // + // 3. "Regular Key" + // A. signingAcctID != signingAcctIDFromPubKey, and signingAcctID + // is in the ledger. + // B. If signingAcctIDFromPubKey == signingAcctID.RegularKey (from + // ledger) then the signature is allowed. + // + // No other signatures are allowed. (January 2015) + + // In any of these cases we need to know whether the account is in + // the ledger. Determine that now. + uint256 const signerAccountIndex = getAccountRootIndex (signingAcctID); + + SLE::pointer signersAccountRoot = + engine->view ().entryCache (ltACCOUNT_ROOT, signerAccountIndex); + + if (signingAcctIDFromPubKey == signingAcctID) + { + // Either Phantom or Master. Phantom's automatically pass. + if (signersAccountRoot) + { + // Master Key. Account may not have asfDisableMaster set. + std::uint32_t const signerAccountFlags = + signersAccountRoot->getFieldU32 (sfFlags); + + if (signerAccountFlags & lsfDisableMaster) + { + journal.trace << + "applyTransaction: MultiSignature lsfDisableMaster."; + ret.ter = tefMASTER_DISABLED; + return ret; + } + } + } + else + { + // May be a Regular Key. Let's find out. + // Public key must hash to the account's regular key. + if (!signersAccountRoot) + { + journal.trace << + "applyTransaction: Non-phantom signer lacks account root."; + ret.ter = tefBAD_SIGNATURE; + return ret; + } + + if (!signersAccountRoot->isFieldPresent (sfRegularKey)) + { + journal.trace << + "applyTransaction: Account lacks RegularKey."; + ret.ter = tefBAD_SIGNATURE; + return ret; + } + if (signingAcctIDFromPubKey != + signersAccountRoot->getFieldAccount160 (sfRegularKey)) + { + journal.trace << + "applyTransaction: Account doesn't match RegularKey."; + ret.ter = tefBAD_SIGNATURE; + return ret; + } + } + // The signer is legitimate. Add their weight toward the quorum. + weightSum += signerEntriesItr->weight; + } + ret.weightSum = weightSum; + ret.ter = tesSUCCESS; + return ret; +} + +} // namespace TransactorDetail + +TER Transactor::checkMultiSign () +{ + // Get mTxnAccountID's SignerList and Quorum. + using namespace TransactorDetail; + GetSignerListResult const outer = + getSignerList (mTxnAccountID, mEngine, m_journal); + + if (outer.ter != tesSUCCESS) + return outer.ter; + + // Get the actual array of transaction signers. + STArray const& multiSigners (mTxn.getFieldArray (sfMultiSigners)); + + // Walk the accountSigners performing a variety of checks and see if + // the quorum is met. + + // Both the multiSigners and accountSigners are sorted by account. So + // matching multi-signers to account signers should be a simple + // linear walk. *All* signers must be valid or the transaction fails. + std::uint32_t weightSum = 0; + auto signerEntriesItr = outer.signerEntries.begin (); + for (auto const& signingFor : multiSigners) + { + Account const signingForID = + signingFor.getFieldAccount (sfAccount).getAccountID (); + + STArray const& signingAccounts = + signingFor.getFieldArray (sfSigningAccounts); + + // There are two possibilities: + // o The signers are direct multi-signers for this account. + // o The signers are signing for a multi-signer on this account. + // Handle those two cases separately. + if (signingForID == mTxnAccountID) + { + // The signers are direct multi-signers for this account. Results + // from these signers directly effect the quorum. + CheckSigningAccountsResult const outerSigningAccountsResult = + checkSigningAccounts ( + outer.signerEntries, signingAccounts, mEngine, m_journal); + + if (outerSigningAccountsResult.ter != tesSUCCESS) + return outerSigningAccountsResult.ter; + + weightSum += outerSigningAccountsResult.weightSum; + } + else + { + // The signers are signing for a multi-signer on this account. + // Attempt to match the signingForID with a SignerEntry + while (signerEntriesItr->account < signingForID) + { + if (++signerEntriesItr == outer.signerEntries.end ()) + { + m_journal.trace << + "applyTransaction: Invalid SigningFor.Account."; + return tefBAD_SIGNATURE; + } + } + if (signerEntriesItr->account != signingForID) + { + // The signingForID is not in the SignerEntries. + m_journal.trace << + "applyTransaction: Invalid SigningFor.Account."; + return tefBAD_SIGNATURE; + } + if (signerEntriesItr->weight <= 0) + { + // The SigningFor entry needs a weight greater than zero. + m_journal.trace << + "applyTransaction: SigningFor.Account needs weight > 0."; + return tefBAD_SIGNATURE; + } + + // See if the signingForID has a SignerList. + GetSignerListResult const inner = + getSignerList (signingForID, mEngine, m_journal); + + if (inner.ter != tesSUCCESS) + return inner.ter; + + // Results from these signers indirectly effect the quorum. + CheckSigningAccountsResult const innerSigningAccountsResult = + checkSigningAccounts ( + inner.signerEntries, signingAccounts, mEngine, m_journal); + + if (innerSigningAccountsResult.ter != tesSUCCESS) + return innerSigningAccountsResult.ter; + + // There's a policy question here. If the SigningAccounts are + // all valid but fail to reach this signingFor's Quorum do we: + // + // 1. Say the signature is valid but contributes 0 toward the + // quorum? + // + // 2. Say that any SigningFor that doesn't meet quorum is an + // invalid signature and fails? + // + // The choice is not obvious to me. I'm picking 2 for now, since + // it's more restrictive. We can switch to policy 1 later without + // causing transactions that would have worked before to fail. + // -- January 2015 + if (innerSigningAccountsResult.weightSum < inner.quorum) + { + m_journal.trace << + "applyTransaction: Level-2 SigningFor did not make quorum."; + return tefBAD_QUORUM; + } + // This SigningFor met quorum. Add its weight. + weightSum += signerEntriesItr->weight; + } + } + + // Cannot perform transaction if quorum is not met. + if (weightSum < outer.quorum) + { + m_journal.trace << + "applyTransaction: MultiSignature failed to meet quorum."; + return tefBAD_QUORUM; + } + + // Met the quorum. Continue. + return tesSUCCESS; +} + } diff --git a/src/ripple/app/tx/impl/Transactor.h b/src/ripple/app/tx/impl/Transactor.h index c2995099e..92531a01e 100644 --- a/src/ripple/app/tx/impl/Transactor.h +++ b/src/ripple/app/tx/impl/Transactor.h @@ -67,7 +67,7 @@ protected: // Returns the fee, not scaled for load (Should be in fee units. FIXME) virtual std::uint64_t calculateBaseFee (); - virtual TER checkSig (); + virtual TER checkSign (); virtual TER doApply () = 0; Transactor ( @@ -80,6 +80,10 @@ protected: { return true; } + +private: + TER checkSingleSign (); + TER checkMultiSign (); }; } diff --git a/src/ripple/app/tx/tests/MultiSign.test.cpp b/src/ripple/app/tx/tests/MultiSign.test.cpp new file mode 100644 index 000000000..700ca6f17 --- /dev/null +++ b/src/ripple/app/tx/tests/MultiSign.test.cpp @@ -0,0 +1,1618 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include + +namespace ripple { +namespace test { + +class MultiSign_test : public beast::unit_test::suite +{ + static std::uint64_t const xrp = std::mega::num; + static int const stdFee = 10; + + // Unfunded accounts to use for phantom signing. + UserAccount bogie {KeyType::secp256k1, "bogie"}; + UserAccount ghost {KeyType::ed25519, "ghost"}; + UserAccount haunt {KeyType::secp256k1, "haunt"}; + UserAccount jinni {KeyType::ed25519, "jinni"}; + UserAccount shade {KeyType::secp256k1, "shade"}; + UserAccount spook {KeyType::ed25519, "spook"}; + +//------------------------------------------------------------------------------ + + void test_singleSig (KeyType kType) + { + UserAccount master (kType, "masterpassphrase"); + + TestLedger ledger (100000*xrp, master, *this); + + // User accounts. + std::uint64_t aliceBalance = 1000000000; + UserAccount alice (kType, "alice"); + + // This lambda makes it easy to check alice's balance. + auto aliceBalanceCheck = + [&ledger, &alice, &aliceBalance, this](std::uint64_t balanceChange) + { + aliceBalance -= balanceChange; + this->expect(getNativeBalance (ledger, alice) == aliceBalance); + }; + + payInDrops (ledger, master, alice, aliceBalance); + aliceBalanceCheck (0); + + // Pay from alice to master, but alice doesn't sign. Should fail. + { + STTx tx = getPaymentTx (alice, master, 990); + singleSign (tx, bogie); + ledger.applyBadTransaction (tx, tefBAD_AUTH_MASTER); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Pay from alice to master using alice's master key. + payInDrops (ledger, alice, master, 1000 - stdFee); + aliceBalanceCheck (1000); + + // Give alice a regular key. + alice.setRegKey (ledger, kType, "alie"); + aliceBalanceCheck (stdFee); + + // Make another payment to master, but still use the master key. + payInDrops (ledger, alice, master, 1000 - stdFee); + aliceBalanceCheck (1000); + + // Tell alice to use the regular key and make another payment. + alice.useRegKey (true); + payInDrops (ledger, alice, master, 1000 - stdFee); + aliceBalanceCheck (1000); + + // Disable alice's master key. + alice.useRegKey (false); + alice.disableMaster (ledger, true); + aliceBalanceCheck (stdFee); + + // Have alice make another payment with her master key. Should fail. + { + STTx tx = getPaymentTx (alice, master, 1000 - stdFee); + singleSign (tx, alice); + ledger.applyBadTransaction (tx, tefMASTER_DISABLED); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + + // alice makes another payment with her regular key. Should succeed. + alice.useRegKey (true); + payInDrops (ledger, alice, master, 1000 - stdFee); + aliceBalanceCheck (1000); + } + +//------------------------------------------------------------------------------ + + void test_noReserve (KeyType kType) + { + UserAccount master (kType, "masterpassphrase"); + + TestLedger ledger (100000*xrp, master, *this); + + // Pay alice enough to meet the initial reserve, but not enough to + // meet the reserve for a SignerListSet. + std::uint64_t aliceBalance = 200000000; + UserAccount alice (kType, "alice"); + + // This lambda makes it easy to check alice's balance. + auto aliceBalanceCheck = + [&ledger, &alice, &aliceBalance, this](std::uint64_t balanceChange) + { + aliceBalance -= balanceChange; + this->expect(getNativeBalance (ledger, alice) == aliceBalance); + }; + + payInDrops (ledger, master, alice, aliceBalance); + aliceBalanceCheck (0); + + // Create a signerlist that we can attach to alice. + SignerList aliceSigners {{bogie, 1}, {ghost, 2}, {haunt, 3}}; + { + STTx tx = getSignerListSetTx (alice, aliceSigners, 3); + singleSign (tx, alice); + ledger.applyTecTransaction (tx, tecINSUFFICIENT_RESERVE); + aliceBalanceCheck (stdFee); + } + // Fund alice better. SignerListSet should succeed now. + aliceBalance += 1000000000; + payInDrops (ledger, master, alice, 1000000000); + aliceBalanceCheck (0); + { + STTx tx = getSignerListSetTx (alice, aliceSigners, 3); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + } + +//------------------------------------------------------------------------------ + + void test_signerListSet (KeyType kType) + { + UserAccount master (kType, "masterpassphrase"); + + TestLedger ledger (100000*xrp, master, *this); + + // User accounts + UserAccount alice (kType, "alice"); + std::uint64_t aliceBalance = 1000*xrp; + + // This lambda makes it easy to check alice's balance. + auto aliceBalanceCheck = + [&ledger, &alice, &aliceBalance, this](std::uint64_t balanceChange) + { + aliceBalance -= balanceChange; + this->expect(getNativeBalance (ledger, alice) == aliceBalance); + }; + + payInDrops (ledger, master, alice, aliceBalance); + aliceBalanceCheck (0); + + // Attach a signer to alice. Should fail since there's only one signer. + { + SignerList aliceSigners {{bogie, 3}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 3); + singleSign (tx, alice); + ledger.applyBadTransaction (tx, temMALFORMED); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + expect(getOwnerCount (ledger, alice) == 0); + } + // Try again with two multi-signers. Should work. + { + SignerList aliceSigners {{bogie, 3}, {ghost, 3}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 6); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + expect(getOwnerCount (ledger, alice) == 4); + } + // Try to add alice as a multi-signer on her own account. Should fail. + { + SignerList aliceSigners {{alice, 3}, {bogie, 3}, {ghost, 3}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 1); + singleSign (tx, alice); + ledger.applyBadTransaction (tx, temBAD_SIGNER); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + expect(getOwnerCount (ledger, alice) == 4); + } + // Try to add the same account twice. Should fail. + { + SignerList aliceSigners { + {bogie, 3}, {ghost, 3}, {haunt, 3}, {shade, 3}, {ghost, 3}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 1); + singleSign (tx, alice); + ledger.applyBadTransaction (tx, temBAD_SIGNER); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + expect(getOwnerCount (ledger, alice) == 4); + } + // Set a signer list where the quorum can't be met. Should fail. + { + SignerList aliceSigners {{bogie, 3}, {ghost, 3}, {haunt, 3}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 10); + singleSign (tx, alice); + ledger.applyBadTransaction (tx, temBAD_QUORUM); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + expect(getOwnerCount (ledger, alice) == 4); + } + // Try setting a signer list where the quorum can barely be met. Also, + // set a weight of zero, which is legal. + { + SignerList aliceSigners{{bogie, 0}, {ghost, 65535}, {haunt, 65535}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 131070); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + expect(getOwnerCount (ledger, alice) == 5); + } + // Try a zero quorum. Should fail. + { + SignerList aliceSigners{{bogie, 0}, {ghost, 0}, {haunt, 0}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 0); + singleSign (tx, alice); + ledger.applyBadTransaction (tx, temMALFORMED); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + expect(getOwnerCount (ledger, alice) == 5); + } + + // Try to create a signer list that's barely too big. Should fail. + UserAccount becky (kType, "becky"); + payInDrops (ledger, master, becky, 1000*xrp); + + UserAccount cheri (kType, "cheri"); + payInDrops (ledger, master, cheri, 1000*xrp); + + UserAccount daria (kType, "daria"); + payInDrops (ledger, master, daria, 1000*xrp); + { + SignerList aliceSigners{{bogie, 1}, {ghost, 1}, {haunt, 1}, + {jinni, 1}, {shade, 1}, {spook, 1}, {becky, 1}, {cheri, 1}, + {daria, 1}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 1); + singleSign (tx, alice); + ledger.applyBadTransaction (tx, temMALFORMED); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + expect(getOwnerCount (ledger, alice) == 5); + } + // Make the biggest allowed list. This one should succeed. + { + SignerList aliceSigners{{bogie, 1}, {ghost, 1}, {haunt, 1}, + {jinni, 1}, {shade, 1}, {spook, 1}, {becky, 1}, {cheri, 1}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 1); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + expect(getOwnerCount (ledger, alice) == 10); + } + // Remove alice's SignerList. Should succeed. + { + SignerList aliceSigners; + STTx tx = getSignerListSetTx (alice, aliceSigners, 0); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + expect(getOwnerCount (ledger, alice) == 0); + } + } + +//------------------------------------------------------------------------------ + + void test_phantomSigners (KeyType kType) + { + UserAccount master (kType, "masterpassphrase"); + + TestLedger ledger (100000*xrp, master, *this); + + // User accounts + UserAccount alice (kType, "alice"); + std::uint64_t aliceBalance = 1000*xrp; + + // This lambda makes it easy to check alice's balance. + auto aliceBalanceCheck = + [&ledger, &alice, &aliceBalance, this](std::uint64_t balanceChange) + { + aliceBalance -= balanceChange; + this->expect(getNativeBalance (ledger, alice) == aliceBalance); + }; + + payInDrops (ledger, master, alice, aliceBalance); + aliceBalanceCheck (0); + + // Attach phantom signers to alice. Should work. + { + SignerList aliceSigners {{bogie, 3}, {ghost, 3}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 6); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Make a multi-signed payment from alice to master. Should succeed. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, bogie, tx}, + {alice, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + + // You can't re-use signatures on a new transaction. Verify that. + STTx badTx = getPaymentTx (alice, master, 1000 - stdFee); + multiSign (badTx, multiSigs); + ledger.applyBadTransaction (badTx, temINVALID); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Try mal-ordered signers. Should fail. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector badSigs { + {alice, bogie, tx}, + {alice, ghost, tx} + }; + std::sort (badSigs.begin(), badSigs.end()); + std::reverse (badSigs.begin(), badSigs.end()); + insertMultiSigs (tx, badSigs); + ledger.applyBadTransaction (tx, temINVALID); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Try duplicate signers. Should fail. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector badSigs { + {alice, ghost, tx}, + {alice, ghost, tx} + }; + multiSign (tx, badSigs); + ledger.applyBadTransaction (tx, temINVALID); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Both single- and and multi-sign. Should fail. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, bogie, tx}, + {alice, ghost, tx} + }; + singleSign (tx, alice); + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, temINVALID); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Don't meet the quorum. Should fail. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector badSigs { + {alice, bogie, tx} + }; + multiSign (tx, badSigs); + ledger.applyBadTransaction (tx, tefBAD_QUORUM); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Multi-sign where one of the signers is not valid. Should fail. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, bogie, tx}, + {alice, ghost, tx}, + {alice, haunt, tx} + }; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, tefBAD_SIGNATURE); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + } + +//------------------------------------------------------------------------------ + + void test_masterSigners (KeyType kType) + { + UserAccount master (kType, "masterpassphrase"); + + TestLedger ledger (100000*xrp, master, *this); + + // User accounts + UserAccount alice (kType, "alice"); + std::uint64_t aliceBalance = 1000*xrp; + + // This lambda makes it easy to check alice's balance. + auto aliceBalanceCheck = + [&ledger, &alice, &aliceBalance, this](std::uint64_t balanceChange) + { + aliceBalance -= balanceChange; + this->expect(getNativeBalance (ledger, alice) == aliceBalance); + }; + + payInDrops (ledger, master, alice, aliceBalance); + aliceBalanceCheck (0); + + UserAccount becky (kType, "becky"); + payInDrops (ledger, master, becky, 1000*xrp); + + UserAccount cheri (kType, "cheri"); + payInDrops (ledger, master, cheri, 1000*xrp); + + UserAccount daria (kType, "daria"); + payInDrops (ledger, master, daria, 1000*xrp); + + // To mix things up, give alice a regular key, but don't use it. + alice.setRegKey (ledger, kType, "alie"); + aliceBalanceCheck (stdFee); + + // Attach signers to alice. Should work. + { + SignerList aliceSigners {{becky, 3}, {cheri, 4}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 7); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Make a multi-signed payment from alice to master. Should succeed. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, cheri, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + // Attempt a multi-signed transaction that doesn't meet the quorum + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, cheri, tx} + }; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, tefBAD_QUORUM); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Attempt a multi-signed transaction where one signer is not valid. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, cheri, tx}, + {alice, daria, tx} + }; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, tefBAD_SIGNATURE); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Give becky and cheri regular keys but don't use them. Should work. + { + becky.setRegKey (ledger, kType, "beck"); + cheri.setRegKey (ledger, kType, "cher"); + + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, cheri, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + } + +//------------------------------------------------------------------------------ + + void test_regularSigners (KeyType kType) + { + UserAccount master (kType, "masterpassphrase"); + + TestLedger ledger (100000*xrp, master, *this); + + // User accounts. Have everyone use regular keys. + UserAccount alice (kType, "alice"); + std::uint64_t aliceBalance = 1000*xrp; + + // This lambda makes it easy to check alice's balance. + auto aliceBalanceCheck = + [&ledger, &alice, &aliceBalance, this](std::uint64_t balanceChange) + { + aliceBalance -= balanceChange; + this->expect(getNativeBalance (ledger, alice) == aliceBalance); + }; + + payInDrops (ledger, master, alice, aliceBalance); + aliceBalanceCheck (0); + + alice.setRegKey (ledger, kType, "alie"); + alice.useRegKey (true); + aliceBalanceCheck (stdFee); + + UserAccount becky (kType, "becky"); + payInDrops (ledger, master, becky, 1000*xrp); + becky.setRegKey (ledger, kType, "beck"); + becky.useRegKey (true); + + // Disable cheri's master key to mix things up. + UserAccount cheri (kType, "cheri"); + payInDrops (ledger, master, cheri, 1000*xrp); + cheri.setRegKey (ledger, kType, "cher"); + cheri.disableMaster(ledger, true); + cheri.useRegKey (true); + + UserAccount daria (kType, "daria"); + payInDrops (ledger, master, daria, 1000*xrp); + daria.setRegKey (ledger, kType, "darr"); + daria.useRegKey (true); + + // Attach signers to alice. Should work. + { + SignerList aliceSigners {{becky, 3}, {cheri, 4}}; + STTx tx = getSignerListSetTx (alice, aliceSigners, 7); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + + // Make a multi-signed payment from alice to master. Should succeed. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, cheri, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + + // Attempt a multi-signed transaction that doesn't meet the quorum + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, cheri, tx} + }; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, tefBAD_QUORUM); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + + // Attempt a multi-signed transaction where one signer is not valid. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, cheri, tx}, + {alice, daria, tx} + }; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, tefBAD_SIGNATURE); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + + // Have becky sign with her master key and then disable the + // master before we submit the transaction. Should fail. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + becky.useRegKey (false); + std::vector multiSigs { + {alice, becky, tx}, + {alice, cheri, tx} + }; + multiSign (tx, multiSigs); + becky.disableMaster(ledger, true); + becky.useRegKey (true); + ledger.applyBadTransaction (tx, tefMASTER_DISABLED); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + + // Now that becky is using her regular key her signature should succeed. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, cheri, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + } + +//------------------------------------------------------------------------------ + + void test_heterogeneousSigners (KeyType kType) + { + UserAccount master (kType, "masterpassphrase"); + + TestLedger ledger (100000*xrp, master, *this); + + // User accounts. alice uses a regular key with the master disabled. + UserAccount alice (kType, "alice"); + std::uint64_t aliceBalance = 1000*xrp; + + // This lambda makes it easy to check alice's balance. + auto aliceBalanceCheck = + [&ledger, &alice, &aliceBalance, this](std::uint64_t balanceChange) + { + aliceBalance -= balanceChange; + this->expect(getNativeBalance (ledger, alice) == aliceBalance); + }; + + payInDrops (ledger, master, alice, aliceBalance); + aliceBalanceCheck (0); + alice.setRegKey (ledger, kType, "alie"); + aliceBalanceCheck (stdFee); + + alice.disableMaster (ledger, true); + aliceBalanceCheck (stdFee); + + alice.useRegKey (true); + + // becky is master only, without a regular key. + UserAccount becky (kType, "becky"); + payInDrops (ledger, master, becky, 1000*xrp); + + // cheri is master, but with a regular key. + UserAccount cheri (kType, "cheri"); + payInDrops (ledger, master, cheri, 1000*xrp); + cheri.setRegKey (ledger, kType, "cher"); + + // daria uses her regular key, but leaves the master enabled. + UserAccount daria (kType, "daria"); + payInDrops (ledger, master, daria, 1000*xrp); + daria.setRegKey (ledger, kType, "dar"); + cheri.useRegKey (true); + + // edith disables the master and uses her regular key. + UserAccount edith (kType, "edith"); + payInDrops (ledger, master, edith, 1000*xrp); + edith.setRegKey (ledger, kType, "edi"); + edith.disableMaster(ledger, true); + edith.useRegKey (true); + + SignerList aliceSigners { + {becky, 1}, + {cheri, 1}, + {daria, 1}, + {edith, 1}, + {ghost, 1}, + {haunt, 0} + }; + // Attach signers to alice. + { + STTx tx = getSignerListSetTx (alice, aliceSigners, 1); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Each type of signer (with weight) should succeed individually. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{alice, becky, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{alice, cheri, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{alice, daria, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{alice, edith, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{alice, ghost, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + // Should also be no sweat if all of the signers (with weight) sign. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, cheri, tx}, + {alice, daria, tx}, + {alice, edith, tx}, + {alice, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + // But the transaction should fail if a zero-weight signer is included. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, cheri, tx}, + {alice, daria, tx}, + {alice, edith, tx}, + {alice, ghost, tx}, + {alice, haunt, tx} + }; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, tefBAD_SIGNATURE); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Require that all weighted signers sign. + { + STTx tx = getSignerListSetTx (alice, aliceSigners, 5); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + { + // Make sure that works. + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, cheri, tx}, + {alice, daria, tx}, + {alice, edith, tx}, + {alice, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + } + +//------------------------------------------------------------------------------ + + void test_twoLevel(KeyType kType) + { + UserAccount master (kType, "masterpassphrase"); + + TestLedger ledger (100000*xrp, master, *this); + + // User accounts. + UserAccount alice (kType, "alice"); + std::uint64_t aliceBalance = 1000*xrp; + + // This lambda makes it easy to check alice's balance. + auto aliceBalanceCheck = + [&ledger, &alice, &aliceBalance, this](std::uint64_t balanceChange) + { + aliceBalance -= balanceChange; + this->expect(getNativeBalance (ledger, alice) == aliceBalance); + }; + + // alice uses a regular key with the master disabled. + payInDrops (ledger, master, alice, 1000*xrp); + aliceBalanceCheck (0); + alice.setRegKey (ledger, kType, "alie"); + aliceBalanceCheck (stdFee); + alice.disableMaster (ledger, true); + aliceBalanceCheck (stdFee); + alice.useRegKey (true); + + // becky is master only, without a regular key. + UserAccount becky (kType, "becky"); + payInDrops (ledger, master, becky, 1000*xrp); + + // cheri is master, but with a regular key. + UserAccount cheri (kType, "cheri"); + payInDrops (ledger, master, cheri, 1000*xrp); + cheri.setRegKey (ledger, kType, "cher"); + + // daria uses her regular key, but leaves the master enabled. + UserAccount daria (kType, "daria"); + payInDrops (ledger, master, daria, 1000*xrp); + daria.setRegKey (ledger, kType, "dar"); + cheri.useRegKey (true); + + // edith disables the master and uses her regular key. + UserAccount edith (kType, "edith"); + payInDrops (ledger, master, edith, 1000*xrp); + edith.setRegKey (ledger, kType, "edi"); + edith.disableMaster(ledger, true); + edith.useRegKey (true); + + // Fund four more accounts so alice can have 8 in-ledger signers. + UserAccount freda (kType, "freda"); + payInDrops (ledger, master, freda, 1000*xrp); + + UserAccount ginny (kType, "ginny"); + payInDrops (ledger, master, ginny, 1000*xrp); + + UserAccount helen (kType, "helen"); + payInDrops (ledger, master, helen, 1000*xrp); + + UserAccount irena (kType, "irena"); + payInDrops (ledger, master, irena, 1000*xrp); + + // Attach signers to alice. + SignerList aliceSigners { + {becky, 1}, + {cheri, 1}, + {daria, 1}, + {edith, 1}, + {freda, 1}, + {ginny, 1}, + {helen, 1}, + {irena, 1}, + }; + { + STTx tx = getSignerListSetTx (alice, aliceSigners, 1); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Attach signers to becky. + SignerList beckySigners { + {alice, 1}, + {cheri, 1}, + {daria, 1}, + {edith, 1}, + {freda, 1}, + {ghost, 1}, + {haunt, 0}, + {irena, 1}, + }; + { + STTx tx = getSignerListSetTx (becky, beckySigners, 1); + singleSign (tx, becky); + ledger.applyGoodTransaction (tx); + } + // Attach signers to cheri. + { + SignerList cheriSigners { + {alice, 1}, + {becky, 1}, + {daria, 1}, + {edith, 1}, + {freda, 1}, + {ghost, 1}, + {haunt, 1}, + {irena, 1}, + }; + STTx tx = getSignerListSetTx (cheri, cheriSigners, 8); + singleSign (tx, cheri); + ledger.applyGoodTransaction (tx); + } + // Attach signers to daria. + { + SignerList dariaSigners { + {alice, 1}, + {becky, 1}, + {cheri, 1}, + {edith, 1}, + {freda, 1}, + {ghost, 1}, + {haunt, 1}, + {irena, 1}, + }; + STTx tx = getSignerListSetTx (daria, dariaSigners, 8); + singleSign (tx, daria); + ledger.applyGoodTransaction (tx); + } + // Attach signers to edith. + { + SignerList edithSigners { + {alice, 1}, + {becky, 1}, + {cheri, 1}, + {daria, 1}, + {freda, 1}, + {ghost, 1}, + {haunt, 1}, + {irena, 1}, + }; + STTx tx = getSignerListSetTx (edith, edithSigners, 8); + singleSign (tx, edith); + ledger.applyGoodTransaction (tx); + } + // Attach signers to freda. + { + SignerList fredaSigners { + {alice, 1}, + {becky, 1}, + {cheri, 1}, + {daria, 1}, + {edith, 1}, + {ghost, 1}, + {haunt, 1}, + {irena, 1}, + }; + STTx tx = getSignerListSetTx (freda, fredaSigners, 8); + singleSign (tx, freda); + ledger.applyGoodTransaction (tx); + } + // Attach signers to ginny. + { + SignerList ginnySigners { + {alice, 1}, + {becky, 1}, + {cheri, 1}, + {daria, 1}, + {edith, 1}, + {ghost, 1}, + {haunt, 1}, + {irena, 1}, + }; + STTx tx = getSignerListSetTx (ginny, ginnySigners, 8); + singleSign (tx, ginny); + ledger.applyGoodTransaction (tx); + } + // Attach signers to helen. + { + SignerList helenSigners { + {alice, 1}, + {becky, 1}, + {cheri, 1}, + {daria, 1}, + {edith, 1}, + {ghost, 1}, + {haunt, 1}, + {irena, 1}, + }; + STTx tx = getSignerListSetTx (helen, helenSigners, 8); + singleSign (tx, helen); + ledger.applyGoodTransaction (tx); + } + // Attach signers to irena. + { + SignerList irenaSigners { + {alice, 1}, + {becky, 1}, + {cheri, 1}, + {daria, 1}, + {edith, 1}, + {ghost, 1}, + {haunt, 1}, + {helen, 1}, + }; + STTx tx = getSignerListSetTx (irena, irenaSigners, 8); + singleSign (tx, irena); + ledger.applyGoodTransaction (tx); + } + + // becky signing both directly and through a signer list should fail. + // + // This takes a little explanation. It isn't easy to see in this + // format, but becky is attempting to sign this transaction twice. + // + // o The first one you can see. Becky signs for alice on alice's + // account. + // + // o The second is harder to see. cheri is signing for becky. But + // at the end of the day, it is becky who is signing on alice's + // account even though cheri is signing *for* becky. + // + // If we allow becky to sign both these ways then she would get twice + // as much weight as she is alloted. So we must reject this case. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {becky, cheri, tx} + }; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, temINVALID); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Each type of signer (with weight) should succeed individually. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{becky, alice, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{becky, cheri, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{becky, daria, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{becky, freda, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{becky, ghost, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + // Transaction should fail if becky signs for herself. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{becky, becky, tx}}; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, temINVALID); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Transaction should fail if haunt signs, since haunt has zero weight. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs {{becky, haunt, tx}}; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, tefBAD_SIGNATURE); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Crank up becky's quorum so she needs all signers. Just for fun + // we'll multi-sign it. + { + STTx tx = getSignerListSetTx (becky, beckySigners, 7); + std::vector multiSigs {{becky, alice, tx}}; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + } + // A transaction that's one signature short at the second level fails. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {becky, alice, tx}, + {becky, cheri, tx}, + {becky, daria, tx}, + {becky, edith, tx}, + {becky, freda, tx}, + {becky, ghost, tx} +// {becky, irena, tx} + }; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, tefBAD_QUORUM); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Add in the necessary signature and succeed. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {becky, alice, tx}, + {becky, cheri, tx}, + {becky, daria, tx}, + {becky, edith, tx}, + {becky, freda, tx}, + {becky, ghost, tx}, + {becky, irena, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + // Crank up alice's quorum to try combining 1-level and 2-level signing. + { + STTx tx = getSignerListSetTx (alice, aliceSigners, 8); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Mix levels of signing. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {cheri, alice, tx}, // 2-level signing + {cheri, becky, tx}, + {cheri, daria, tx}, + {cheri, edith, tx}, + {cheri, freda, tx}, + {cheri, ghost, tx}, + {cheri, haunt, tx}, + {cheri, irena, tx}, + {alice, daria, tx}, + {alice, edith, tx}, + {alice, freda, tx}, + {alice, ginny, tx}, + {alice, helen, tx}, + {alice, irena, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + // Replace becky's signer list so we can go for a worst case signature. + { + beckySigners = { + {alice, 1}, + {cheri, 1}, + {daria, 1}, + {edith, 1}, + {freda, 1}, + {ghost, 1}, + {haunt, 1}, + {irena, 1}, + }; + STTx tx = getSignerListSetTx (becky, beckySigners, 8); + singleSign (tx, becky); + ledger.applyGoodTransaction (tx); + } + // Make the grandmother of all 2-level signatures. Should work. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + std::vector multiSigs { + {becky, alice, tx}, // becky + {becky, cheri, tx}, + {becky, daria, tx}, + {becky, edith, tx}, + {becky, freda, tx}, + {becky, ghost, tx}, + {becky, haunt, tx}, + {becky, irena, tx}, + + {cheri, alice, tx}, // cheri + {cheri, becky, tx}, + {cheri, daria, tx}, + {cheri, edith, tx}, + {cheri, freda, tx}, + {cheri, ghost, tx}, + {cheri, haunt, tx}, + {cheri, irena, tx}, + + {daria, alice, tx}, // daria + {daria, becky, tx}, + {daria, cheri, tx}, + {daria, edith, tx}, + {daria, freda, tx}, + {daria, ghost, tx}, + {daria, haunt, tx}, + {daria, irena, tx}, + + {edith, alice, tx}, // edith + {edith, becky, tx}, + {edith, cheri, tx}, + {edith, daria, tx}, + {edith, freda, tx}, + {edith, ghost, tx}, + {edith, haunt, tx}, + {edith, irena, tx}, + + {freda, alice, tx}, // freda + {freda, becky, tx}, + {freda, cheri, tx}, + {freda, daria, tx}, + {freda, edith, tx}, + {freda, ghost, tx}, + {freda, haunt, tx}, + {freda, irena, tx}, + + {ginny, alice, tx}, // ginny + {ginny, becky, tx}, + {ginny, cheri, tx}, + {ginny, daria, tx}, + {ginny, edith, tx}, + {ginny, ghost, tx}, + {ginny, haunt, tx}, + {ginny, irena, tx}, + + {helen, alice, tx}, // helen + {helen, becky, tx}, + {helen, cheri, tx}, + {helen, daria, tx}, + {helen, edith, tx}, + {helen, ghost, tx}, + {helen, haunt, tx}, + {helen, irena, tx}, + + {irena, alice, tx}, // irena + {irena, becky, tx}, + {irena, cheri, tx}, + {irena, daria, tx}, + {irena, edith, tx}, + {irena, ghost, tx}, + {irena, haunt, tx}, + {irena, helen, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + } + +//------------------------------------------------------------------------------ + + // See if every kind of transaction can be successfully multi-signed. + void test_txTypes(KeyType kType) + { + UserAccount master (kType, "masterpassphrase"); + + TestLedger ledger (100000*xrp, master, *this); + + // User accounts. + UserAccount alice (kType, "alice"); + std::uint64_t aliceBalance = 1000*xrp; + + // This lambda makes it easy to check alice's balance. + auto aliceBalanceCheck = + [&ledger, &alice, &aliceBalance, this](std::uint64_t balanceChange) + { + aliceBalance -= balanceChange; + this->expect(getNativeBalance (ledger, alice) == aliceBalance); + }; + + // alice uses a regular key with the master enabled. + payInDrops (ledger, master, alice, 1000*xrp); + aliceBalanceCheck (0); + alice.setRegKey (ledger, kType, "alie"); + aliceBalanceCheck (stdFee); + alice.useRegKey (true); + + // becky uses a regular key with the master disabled. + UserAccount becky (kType, "becky"); + payInDrops (ledger, master, becky, 1000*xrp); + becky.setRegKey (ledger, kType, "alie"); + becky.disableMaster (ledger, true); + becky.useRegKey (true); + + // Attach signers to alice. + { + SignerList aliceSigners { {becky, 1}, {bogie, 1}, }; + STTx tx = getSignerListSetTx (alice, aliceSigners, 2); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Attach signers to becky. + { + SignerList beckySigners { {ghost, 1}, {haunt, 1}, }; + STTx tx = getSignerListSetTx (becky, beckySigners, 1); + singleSign (tx, becky); + ledger.applyGoodTransaction (tx); + } + // 2-level multi-sign a ttPAYMENT. + { + STTx tx = getPaymentTx (alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, bogie, tx}, + {becky, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + // 2-level multi-sign a ttACCOUNT_SET + { + // Multi-sign disable alice's master key. Should fail. + { + STTx tx = getAccountSetTx (alice); + tx.setFieldU32 (sfSetFlag, asfDisableMaster); + std::vector multiSigs { + {alice, bogie, tx}, + {becky, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyTecTransaction (tx, tecNEED_MASTER_KEY); + aliceBalanceCheck (stdFee); + } + // Disable alice's master key. + { + alice.useRegKey (false); + STTx tx = getAccountSetTx (alice); + tx.setFieldU32 (sfSetFlag, asfDisableMaster); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Make sure the master key was disabled. + { + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + singleSign (tx, alice); + ledger.applyBadTransaction (tx, tefMASTER_DISABLED); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Re-enable alice's master key. + { + STTx tx = getAccountSetTx (alice); + tx.setFieldU32 (sfClearFlag, asfDisableMaster); + std::vector multiSigs { + {alice, bogie, tx}, + {becky, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Make sure the master key was enabled. + { + alice.useRegKey (false); + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + alice.useRegKey (true); + } + } + // 2-level multi-sign a ttREGULAR_KEY_SET. + { + // Multi-sign changing alice's regular key. + { + RippleAddress const seed = + RippleAddress::createSeedGeneric("BadNewsBears"); + KeyPair regular = generateKeysFromSeed (kType, seed); + STTx tx = getSetRegularKeyTx ( + alice, regular.publicKey.getAccountID()); + + std::vector multiSigs { + {alice, bogie, tx}, + {becky, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Since we didn't tell the local alice that we changed her + // regular key, she should no longer be able to regular sign. + { + alice.useRegKey (true); + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + singleSign (tx, alice); + ledger.applyBadTransaction (tx, tefBAD_AUTH); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Restore alice's regular key. + { + RippleAddress const seed = + RippleAddress::createSeedGeneric("alie"); + KeyPair regular = generateKeysFromSeed (kType, seed); + STTx tx = getSetRegularKeyTx ( + alice, regular.publicKey.getAccountID()); + + std::vector multiSigs { + {alice, bogie, tx}, + {becky, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Regular signing should work again for alice. + { + alice.useRegKey (true); + STTx tx = getPaymentTx(alice, master, 1000 - stdFee); + singleSign (tx, alice); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + } + // We need a Currency and Issue. The next tests use non-XRP. + ripple::Currency const knuts = to_currency("KNT"); + UserAccount gringots (kType, "Gringots Wizarding Bank"); + payInDrops (ledger, master, gringots, 10000*xrp); + Issue const gringotsKnuts (knuts, gringots.getID()); + + // 2-level multi-sign a ttTRUST_SET transaction. + { + // Sending 5 knuts from gringots to alice should fail without a + // trust line. + { + STAmount const payment (gringotsKnuts, 50); + STTx tx = getPaymentTx (gringots, alice, payment); + singleSign (tx, gringots); + ledger.applyTecTransaction (tx, tecPATH_DRY); + } + // 2-level multi-sign a ttTRUST_SET. + { + STTx tx = getTrustSetTx (alice, gringotsKnuts, 100); + std::vector multiSigs { + {alice, bogie, tx}, + {becky, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // We should now be able to send 50 knuts from gringots to alice. + { + STAmount const payment (gringotsKnuts, 50); + STTx tx = getPaymentTx (gringots, alice, payment); + singleSign (tx, gringots); + ledger.applyGoodTransaction (tx); + } + // Make sure alice got her knuts. + { + std::vector states = + getRippleStates (ledger, alice, gringots); + expect (states.size() == 1); + if (!states.empty()) + { + STAmount const balance = states[0]->getBalance(); + STAmount const expected (gringotsKnuts, 50); + expect (balance == expected); + } + } + } + // 2-level multi-sign ttOFFER_CREATE and ttOFFER_CANCEL transactions. + { + // Values shared by subsections: + STAmount const takerGets (gringotsKnuts, 50); + STAmount const takerPays (50); + std::uint32_t offerSeq = 0; + + // alice has 50 knuts. She'll offer to trade them for 50 XRP. + { + STTx tx = getOfferCreateTx (alice, takerGets, takerPays); + std::vector multiSigs { + {alice, bogie, tx}, + {becky, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Verify that alice has an offer. + { + std::vector offers = + getOffersOnAccount (ledger, alice); + expect (offers.size() == 1); + if (! offers.empty()) + { + SLE::pointer const& offer = offers[0]; + expect (takerGets == offer->getFieldAmount(sfTakerGets)); + expect (takerPays == offer->getFieldAmount(sfTakerPays)); + offerSeq = offer->getFieldU32 (sfSequence); + } + } + // Cancel alice's offer using a multi-signed transaction. + { + STTx tx = getOfferCancelTx (alice, offerSeq); + std::vector multiSigs { + {alice, bogie, tx}, + {becky, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Make sure alice's offer is really gone from the ledger. + { + std::vector offers = + getOffersOnAccount (ledger, alice); + expect (offers.empty() == true); + } + } + // Multi-sign a ttSIGNER_LIST_SET + { + // Give alice a new signer list that bogie can no longer sign. + { + SignerList aliceSigners { {becky, 1}, {ghost, 1}, }; + STTx tx = getSignerListSetTx (alice, aliceSigners, 2); + std::vector multiSigs { + {alice, becky, tx}, + {alice, bogie, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Makes sure that becky and bogie can no longer sign. + { + STTx tx = getPaymentTx (alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, bogie, tx} + }; + multiSign (tx, multiSigs); + ledger.applyBadTransaction (tx, tefBAD_SIGNATURE); + alice.decrSeq (); // Fix up local account sequence number. + aliceBalanceCheck (0); + } + // Make sure that becky and ghost can sign. + { + STTx tx = getPaymentTx (alice, master, 1000 - stdFee); + std::vector multiSigs { + {alice, becky, tx}, + {alice, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (1000); + } + // Put alice's account back the way it was. + { + SignerList aliceSigners { {becky, 1}, {bogie, 1}, }; + STTx tx = getSignerListSetTx (alice, aliceSigners, 2); + std::vector multiSigs { + {alice, becky, tx}, + {alice, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + } +#if RIPPLE_ENABLE_TICKETS + // Multi-sign a ttTICKET_CREATE and cancel it using ttTICKET_CANCEL. + { + uint256 ticketIndex {7}; // Any non-zero value so we see it change. + // Multi-sign to give alice an un-targeted ticket + { + STTx tx = getCreateTicketTx (alice); + std::vector multiSigs { + {alice, bogie, tx}, + {becky, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Make sure Alice has the ticket. + { + std::vector tickets = + getTicketsOnAccount (ledger, alice); + expect (tickets.size() == 1); + if (! tickets.empty()) + { + SLE::pointer const& ticket = tickets[0]; + std::uint32_t const ticketSeq = + ticket->getFieldU32(sfSequence); + // getTicketIndex() hashes the account and seq for the ID. + ticketIndex = getTicketIndex (alice.getID(), ticketSeq); + } + } + // Multi-sign to cancel alice's ticket. + { + STTx tx = getCancelTicketTx (alice, ticketIndex); + std::vector multiSigs { + {alice, bogie, tx}, + {becky, ghost, tx} + }; + multiSign (tx, multiSigs); + ledger.applyGoodTransaction (tx); + aliceBalanceCheck (stdFee); + } + // Make sure the ticket is gone. + { + std::vector tickets = + getTicketsOnAccount (ledger, alice); + expect (tickets.size() == 0); + } + } +#endif // RIPPLE_ENABLE_TICKETS + } + +public: + void run () + { + for (auto kType : {KeyType::secp256k1, KeyType::ed25519}) + { + test_singleSig (kType); +#if RIPPLE_ENABLE_MULTI_SIGN + test_noReserve(kType); + test_noReserve(kType); + test_signerListSet (kType); + test_phantomSigners (kType); + test_masterSigners (kType); + test_regularSigners (kType); + test_heterogeneousSigners (kType); + test_twoLevel (kType); + test_txTypes (kType); +#endif // RIPPLE_ENABLE_MULTI_SIGN + } + } +}; + +BEAST_DEFINE_TESTSUITE(MultiSign,ripple_app,ripple); + +} // test +} // ripple diff --git a/src/ripple/app/tx/tests/common_transactor.cpp b/src/ripple/app/tx/tests/common_transactor.cpp new file mode 100644 index 000000000..9939f2a56 --- /dev/null +++ b/src/ripple/app/tx/tests/common_transactor.cpp @@ -0,0 +1,430 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +UserAccount::UserAccount (KeyType kType, std::string const& passphrase) +{ + RippleAddress const seed = RippleAddress::createSeedGeneric (passphrase); + master_ = generateKeysFromSeed (kType, seed); + acctID_ = master_.publicKey.getAccountID (); +} + +void UserAccount::setRegKey ( + TestLedger& ledger, KeyType kType, std::string const& passphrase) +{ + // Get information for the new regular key. + RippleAddress const seed = RippleAddress::createSeedGeneric(passphrase); + KeyPair regular = generateKeysFromSeed (kType, seed); + + // Tell the ledger what we're up to. + STTx tx = getSetRegularKeyTx (*this, regular.publicKey.getAccountID()); + singleSign (tx, *this); + ledger.applyGoodTransaction (tx); + + // Remember what changed. + regular_ = regular; +} + +void UserAccount::clrRegKey (TestLedger& ledger) +{ + // Tell the ledger what we're up to. + STTx tx = getClearRegularKeyTx (*this); + singleSign (tx, *this); + ledger.applyGoodTransaction (tx); + + // Remember what changed. + RippleAddress temp; + regular_.secretKey = temp; + regular_.publicKey = temp; +} + +void UserAccount::disableMaster (TestLedger& ledger, bool doDisable) +{ + STTx tx = getAccountSetTx (*this); + SField const& field = doDisable ? sfSetFlag : sfClearFlag; + tx.setFieldU32 (field, asfDisableMaster); + singleSign (tx, *this); + ledger.applyGoodTransaction (tx); +} + +//------------------------------------------------------------------------------ + +TestLedger::TestLedger ( + std::uint64_t startAmountDrops, + UserAccount const& master, + beast::unit_test::suite& suite) +: lastClosedLedger_() +, openLedger_() +, suite_(suite) +{ + // To leverage createGenesisLedger from the Ledger tests, we must match + // its interface. + TestAccount const masterAcct {master.publicKey(), master.secretKey(), 0}; + std::tie (lastClosedLedger_, openLedger_) = + createGenesisLedger(startAmountDrops, masterAcct); +} + +std::pair TestLedger::applyTransaction (STTx const& tx, bool check) +{ + // Apply the transaction to the open ledger. + TransactionEngine engine(openLedger_); + auto r = engine.applyTransaction ( + tx, tapOPEN_LEDGER | (check ? tapNONE : tapNO_CHECK_SIGN)); + + // Close the open ledger to see if the transaction was real committed. + // + // In part we close the open ledger so we don't have to think about the + // time sequencing of transactions. Every transaction applied by a + // call to this method gets applied individually. So this transaction + // is guaranteed to be applied before the next one. + close_and_advance(openLedger_, lastClosedLedger_); + suite_.expect (lastClosedLedger_->assertSane() == true); + + // Check for the transaction in the closed ledger. + bool const foundTx = + lastClosedLedger_->hasTransaction(tx.getTransactionID()); + suite_.expect (r.second == foundTx); + + return {r.first, r.second && foundTx}; +} + +void TestLedger::applyGoodTransaction (STTx const& tx, bool check) +{ + auto ret = applyTransaction (tx, check); + suite_.expect (ret.first == tesSUCCESS); + suite_.expect (ret.second == true); +} + +void TestLedger::applyBadTransaction (STTx const& tx, TER err, bool check) +{ + auto ret = applyTransaction (tx, check); + suite_.expect (ret.first == err); + suite_.expect (ret.second == false); +} + +void TestLedger::applyTecTransaction (STTx const& tx, TER err, bool check) +{ + auto ret = applyTransaction (tx, check); + suite_.expect (ret.first == err); + suite_.expect (ret.second == true); +} + +AccountState::pointer +TestLedger::getAccountState (UserAccount const& acct) const +{ + return lastClosedLedger_->getAccountState (acct.acctPublicKey ()); +} + +//------------------------------------------------------------------------------ + +MultiSig::MultiSig(UserAccount const& signingFor, + UserAccount const& signer, STTx const& tx) +: signingFor_(&signingFor) +, signer_(&signer) +, multiSig_() +{ + Serializer s = tx.getMultiSigningData ( + signingFor.acctPublicKey(), signer.acctPublicKey()); + multiSig_ = signer.secretKey().accountPrivateSign (s.getData()); +} + +//------------------------------------------------------------------------------ + +void SignerList::injectInto (STTx& tx) const +{ + // Create the SignerListArray one STObject at a time. + STArray list (list_.size ()); + for (auto const& entry : list_) + { + list.emplace_back(sfSignerEntry); + STObject& obj = list.back(); + obj.reserve (2); + obj.setFieldAccount (sfAccount, entry.acct->getID ()); + obj.setFieldU16 (sfSignerWeight, entry.weight); + obj.setTypeFromSField (sfSignerEntry); + } + // Insert the SignerEntries. + tx.setFieldArray (sfSignerEntries, list); +} + +//------------------------------------------------------------------------------ + +// Single-sign the passed transaction using acct. +void singleSign (STTx& tx, UserAccount& acct) +{ + tx.setFieldVL (sfSigningPubKey, acct.publicKey().getAccountPublic ()); + tx.sign (acct.secretKey ()); +} + +// Multi-sign the passed transaction using multiSigs. +void multiSign (STTx& tx, std::vector& multiSigs) +{ + // multiSigs must be sorted or the signature will fail. + std::sort(multiSigs.begin(), multiSigs.end()); + insertMultiSigs(tx, multiSigs); +} + +// Insert the multiSigs into tx without sorting. Allows testing error cases. +void insertMultiSigs (STTx& tx, std::vector const& multiSigs) +{ + // Know when to change out SigningFor containers. + Account prevSigningForID; + + // Create the MultiSigners array one STObject at a time. + STArray multiSigners; + boost::optional signingFor; + for (auto const& entry : multiSigs) + { + if (entry.signingForAccount() != prevSigningForID) + { + if (signingFor != boost::none) + multiSigners.push_back (std::move(signingFor.get())); + + // Construct the next SigningFor object and fill it in. + prevSigningForID = entry.signingForAccount(); + signingFor.emplace (sfSigningFor); + signingFor->reserve (2); + signingFor->setFieldAccount (sfAccount, entry.signingForAccount()); + signingFor->setFieldArray (sfSigningAccounts, STArray()); + } + assert(signingFor); + + // Construct this SigningAccount object and fill it in. + STArray& signingAccounts = signingFor->peekFieldArray( + sfSigningAccounts); + signingAccounts.emplace_back (sfSigningAccount); + + STObject& signingAccount (signingAccounts.back()); + signingAccount.reserve (3); + signingAccount.setFieldAccount (sfAccount, entry.signingAccount()); + signingAccount.setFieldVL (sfMultiSignature, entry.multiSignature()); + signingAccount.setFieldVL (sfSigningPubKey, entry.signingPubKey()); + } + // Remember to put in the final SigningFor object. + if (signingFor) + multiSigners.push_back (std::move(signingFor.get())); + + // Inject the multiSigners into tx. + tx.setFieldArray (sfMultiSigners, multiSigners); +} + +//------------------------------------------------------------------------------ + +// Return a transaction with an SOTemplate, sfTransactionType, sfAccount, +// sfFee, sfFlags, and sfSequence. +STTx getSeqTx (UserAccount& acct, TxType type) +{ + STTx tx (type); // Sets SOTemplate and sfTransactionType. + tx.setFieldAccount (sfAccount, acct.getID()); + tx.setFieldAmount (sfFee, STAmount (10)); + tx.setFieldU32 (sfFlags, tfUniversal); + tx.setFieldU32 (sfSequence, acct.consumeSeq()); + return tx; +} + +// Return an unsigned AccountSet transaction. +STTx getAccountSetTx (UserAccount& acct) +{ + STTx tx = getSeqTx (acct, ttACCOUNT_SET); + return tx; +} + +// Return an unsigned OfferCreate transaction. +STTx getOfferCreateTx (UserAccount& acct, + STAmount const& takerGets, STAmount const& takerPays) +{ + STTx tx = getSeqTx (acct, ttOFFER_CREATE); + tx.setFieldAmount (sfTakerGets, takerGets); + tx.setFieldAmount (sfTakerPays, takerPays); + return tx; +} + +// Return an unsigned OfferCancel transaction. +STTx getOfferCancelTx (UserAccount& acct, std::uint32_t offerSeq) +{ + STTx tx = getSeqTx (acct, ttOFFER_CANCEL); + tx.setFieldU32 (sfOfferSequence, offerSeq); + return tx; +} + +// Return an unsigned transaction good for making a payment. +STTx getPaymentTx ( + UserAccount& from, UserAccount const& to, std::uint64_t amountDrops) +{ + STTx tx = getSeqTx (from, ttPAYMENT); + tx.setFieldAccount (sfDestination, to.getID()); + tx.setFieldAmount (sfAmount, amountDrops); + return tx; +} + +STTx getPaymentTx ( + UserAccount& from, UserAccount const& to, STAmount const& amount) +{ + STTx tx = getSeqTx (from, ttPAYMENT); + tx.setFieldAccount (sfDestination, to.getID()); + tx.setFieldAmount (sfAmount, amount); + return tx; +} + +// Return a transaction that sets a regular key +STTx getSetRegularKeyTx (UserAccount& acct, Account const& regKey) +{ + STTx tx = getSeqTx (acct, ttREGULAR_KEY_SET); + tx.setFieldAccount (sfRegularKey, regKey); + return tx; +} + +// Return a transaction that clears a regular key +STTx getClearRegularKeyTx (UserAccount& acct) +{ + STTx tx = getSeqTx (acct, ttREGULAR_KEY_SET); + return tx; +} + +// Return a SignerListSet transaction. +STTx getSignerListSetTx ( + UserAccount& acct, SignerList const& signers, std::uint32_t quorum) +{ + STTx tx = getSeqTx (acct, ttSIGNER_LIST_SET); + tx.setFieldU32 (sfSignerQuorum, quorum); + if (! signers.empty ()) + { + signers.injectInto (tx); + } + return tx; +} + +// Return a transaction that creates an un-targeted ticket. +STTx getCreateTicketTx (UserAccount& acct) +{ + STTx tx = getSeqTx (acct, ttTICKET_CREATE); + return tx; +} + +// Return a transaction that creates a targeted ticket. +STTx getCreateTicketTx (UserAccount& acct, UserAccount const& target) +{ + STTx tx = getSeqTx (acct, ttTICKET_CREATE); + tx.setFieldAccount (sfTarget, target.getID()); + return tx; +} + +// Return a transaction that cancels a ticket. +STTx getCancelTicketTx (UserAccount& acct, uint256 const& ticketID) +{ + STTx tx = getSeqTx (acct, ttTICKET_CANCEL); + tx.setFieldH256 (sfTicketID, ticketID); + return tx; +} + +// Return a trust set transaction. +STTx getTrustSetTx (UserAccount& from, Issue const& issuer, int limit) +{ + STTx tx = getSeqTx (from, ttTRUST_SET); + STAmount const stLimit (issuer, limit); + tx.setFieldAmount (sfLimitAmount, stLimit); + return tx; +} + +//------------------------------------------------------------------------------ + +void payInDrops (TestLedger& ledger, + UserAccount& from, UserAccount const& to, std::uint64_t amountDrops) +{ + STTx tx = getPaymentTx (from, to, amountDrops); + singleSign (tx, from); + ledger.applyGoodTransaction (tx); +} + +std::uint64_t getNativeBalance(TestLedger& ledger, UserAccount& acct) +{ + return getNValue(ledger.getAccountState(acct)->getBalance()); +} + +std::uint32_t getOwnerCount(TestLedger& ledger, UserAccount& acct) +{ + return ledger.getAccountState(acct)->getSLE()->getFieldU32 (sfOwnerCount); +} + +std::vector getRippleStates ( + TestLedger& ledger, UserAccount const& acct, UserAccount const& peer) +{ + std::vector states; + + ledger.openLedger()->visitAccountItems ( + acct.getID(), + [&states, &acct, &peer](SLE::ref sleCur) + { + // See whether this SLE is a lt_RIPPLE_STATE + if (!sleCur || sleCur->getType () != ltRIPPLE_STATE) + return; + + // It's a lt_RIPPLE_STATE. See if it's one we want to return. + RippleState::pointer const state ( + RippleState::makeItem (acct.getID(), sleCur)); + + if ((state) && (state->getAccountIDPeer() == peer.getID())) + states.emplace_back (std::move (state)); + }); + + return states; +} + +std::vector +getOffersOnAccount (TestLedger& ledger, UserAccount const& acct) +{ + std::vector offers; + + ledger.openLedger()->visitAccountItems ( + acct.getID(), + [&offers, &acct](SLE::ref sleCur) + { + // If sleCur is an ltOFFER save it. + if (sleCur && sleCur->getType () == ltOFFER) + offers.emplace_back (sleCur); + }); + return offers; +} + +std::vector +getTicketsOnAccount (TestLedger& ledger, UserAccount const& acct) +{ + std::vector offers; + + ledger.openLedger()->visitAccountItems ( + acct.getID(), + [&offers, &acct](SLE::ref sleCur) + { + // If sleCur is an ltTICKET save it. + if (sleCur && sleCur->getType () == ltTICKET) + offers.emplace_back (sleCur); + }); + return offers; +} + +} // test +} // ripple diff --git a/src/ripple/app/tx/tests/common_transactor.h b/src/ripple/app/tx/tests/common_transactor.h new file mode 100644 index 000000000..3b4933e80 --- /dev/null +++ b/src/ripple/app/tx/tests/common_transactor.h @@ -0,0 +1,360 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_TRANSACTORS_TESTS_COMMON_TRANSACTOR_H_INCLUDED +#define RIPPLE_APP_TRANSACTORS_TESTS_COMMON_TRANSACTOR_H_INCLUDED + +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +// Forward declare TestLedger so UserAccount can take it as a parameter. +class TestLedger; + +// This class makes it easier to write unit tests that involve user accounts. +class UserAccount +{ +private: + KeyPair master_; + Account acctID_; + KeyPair regular_; + bool useRegKey_ = false; + std::uint32_t sequence_ = 0; + +public: + UserAccount () = delete; + UserAccount (UserAccount const& rhs) = default; + UserAccount& operator= (UserAccount const& rhs) = default; + + UserAccount (KeyType kType, std::string const& passphrase); + + Account const& getID () const + { + return acctID_; + } + + // Sets the regular key on the account, but does not disable master. + void setRegKey ( + TestLedger& ledger, KeyType kType, std::string const& passphrase); + + // Removes the regular key. + void clrRegKey (TestLedger& ledger); + + // Either disables or enables the master key. + void disableMaster (TestLedger& ledger, bool doDisable); + + // Select to use either the regular (true) or master (false) key. + void useRegKey (bool useReg) + { + useRegKey_ = useReg; + } + + std::uint32_t consumeSeq () + { + return ++sequence_; + } + + // If a transaction fails we have to back up the sequence number, since. + // the last sequence wasn't consumed. + void decrSeq () + { + --sequence_; + } + + RippleAddress const& acctPublicKey () const + { + return master_.publicKey; + } + + RippleAddress const& publicKey () const + { + return useRegKey_ ? regular_.publicKey : master_.publicKey; + } + + RippleAddress const& secretKey () const + { + return useRegKey_ ? regular_.secretKey : master_.secretKey; + } +}; + +// This class collects a bunch of the ledger shenanigans tests have to do. +class TestLedger +{ +private: + Ledger::pointer lastClosedLedger_; + Ledger::pointer openLedger_; + beast::unit_test::suite& suite_; + +public: + TestLedger () = delete; + TestLedger (const TestLedger& lhs) = delete; + TestLedger ( + std::uint64_t startAmountDrops, + UserAccount const& master, + beast::unit_test::suite& suite); + + // Return.first : transaction's TEC + // Return.second : transaction successfully applied and in ledger. + std::pair applyTransaction (STTx const& tx, bool check = true); + + // Apply a transaction that we expect to succeed. + void applyGoodTransaction (STTx const& tx, bool check = true); + + // Apply a transaction that we expect to fail. Pass the expected error code. + void applyBadTransaction (STTx const& tx, TER err, bool check = true); + + // apply a transaction that we expect to fail but charges a fee. Pass the + // expected error code. + void applyTecTransaction (STTx const& tx, TER err, bool check = true); + + // Return the AccountState for a UserAccount + AccountState::pointer getAccountState (UserAccount const& acct) const; + + // Return the current open ledger. + Ledger::pointer openLedger () + { + return openLedger_; + } +}; + +// This class makes it easier to construct unit tests with SignerLists. +// +// Typically construct this class with an initializer list. Like this: +// +// UserAccount alice; +// UserAccount becky; +// SignerList s {{alice, 5}, {becky, 2}}; +// +// where '5' and '2' are the weights of the accounts in the SignerList. +class SignerList +{ +private: + // Our initializer list takes references that we turn into pointers. + struct InitListEntry + { + UserAccount& acct; + std::uint16_t weight; + }; + + struct SignerAndWeight + { + UserAccount* acct; + std::uint16_t weight; + SignerAndWeight(InitListEntry& entry) + : acct (&entry.acct) + , weight (entry.weight) { } + }; + std::vector list_; + +public: + SignerList() = default; + SignerList(SignerList const& rhs) = default; + SignerList(SignerList&& rhs) // Visual Studio 2013 won't default move ctor + : list_(std::move(rhs.list_)) + { } + SignerList(std::initializer_list args) + { + list_.reserve(args.size()); + for (auto arg : args) + { + list_.emplace_back(arg); + } + } + + SignerList& operator=(SignerList const& rhs) = default; + SignerList& operator=(SignerList&& rhs) // VS 2013 won't default move assign + { + list_ = std::move(rhs.list_); + return *this; + } + + bool empty() const + { + return list_.empty (); + } + + // Inject this SignerList into the passed transaction. + void injectInto (STTx& tx) const; +}; + +// This type makes it easier to write unit tests that use multi-signatures. +// +// The MultiSig type is intended to be short-lived (any user account it +// references should out-live it). It is also intended to be easily +// moved in a std::vector, so it uses pointers not references. +class MultiSig +{ + UserAccount const* signingFor_; + UserAccount const* signer_; + Blob multiSig_; + +public: + MultiSig() = delete; + MultiSig(MultiSig const& rhs) = default; + MultiSig(MultiSig&& rhs) // Visual Studio 2013 won't default move ctor + : signingFor_ (std::move(rhs.signingFor_)) + , signer_ (std::move(rhs.signer_)) + , multiSig_ (std::move(rhs.multiSig_)) + { } + + MultiSig& operator=(MultiSig const& rhs) = delete; + MultiSig& operator=(MultiSig&& rhs) // VS 2013 won't default move assign + { + signingFor_ = std::move(rhs.signingFor_); + signer_ = std::move(rhs.signer_); + multiSig_ = std::move(rhs.multiSig_); + return *this; + } + + MultiSig(UserAccount const& signingFor, + UserAccount const& signer, STTx const& tx); + + friend bool operator==(MultiSig const& lhs, MultiSig const& rhs) + { + return ((lhs.signingFor_->getID() == rhs.signingFor_->getID()) && + (lhs.signer_->getID() == rhs.signer_->getID())); + } + + friend bool operator<(MultiSig const& lhs, MultiSig const& rhs) + { + Account const& lhsSigingFor = lhs.signingFor_->getID(); + Account const& rhsSigningFor = rhs.signingFor_->getID(); + if (lhsSigingFor < rhsSigningFor) + return true; + + if (lhsSigingFor > rhsSigningFor) + return false; + + if (lhs.signer_->getID() < rhs.signer_->getID()) + return true; + + return false; + } + + Account const& signingForAccount() const + { + return signingFor_->getID(); + } + + Account const& signingAccount() const + { + return signer_->getID(); + } + + Blob const& multiSignature() const + { + return multiSig_; + } + + Blob const& signingPubKey() const + { + return signer_->publicKey().getAccountPublic(); + } +}; + +//------------------------------------------------------------------------------ + +// Single-sign the passed transaction using acct. +void singleSign (STTx& tx, UserAccount& acct); + +// Multi-sign the passed transaction using multiSigs. +void multiSign (STTx& tx, std::vector& multiSigs); + +// Insert the multiSigs into tx without sorting. Allows testing error cases. +void insertMultiSigs (STTx& tx, std::vector const& multiSigs); + +//------------------------------------------------------------------------------ + +// Return a transaction with an SOTemplate, sfTransactionType, sfAccount, +// sfFee, sfFlags, and sfSequence. +STTx getSeqTx (UserAccount& acct, TxType type); + +// Get an AccountSet transaction. +STTx getAccountSetTx (UserAccount& acct); + +// Return an unsigned OfferCreate transaction. +STTx getOfferCreateTx (UserAccount& acct, + STAmount const& takerGets, STAmount const& takerPays); + +// Return an unsigned OfferCancel transaction. +STTx getOfferCancelTx (UserAccount& acct, std::uint32_t offerSeq); + +// Return an unsigned transaction good for making a payment in XRP. +STTx getPaymentTx ( + UserAccount& from, UserAccount const& to, std::uint64_t amountDrops); + +// Return an unsigned transaction good for making a payment. +STTx getPaymentTx ( + UserAccount& from, UserAccount const& to, STAmount const& amount); + +// Return a transaction that sets a regular key. +STTx getSetRegularKeyTx (UserAccount& acct, Account const& regKey); + +// Return a transaction that clears a regular key. +STTx getClearRegularKeyTx (UserAccount& acct); + +// Return a SignerListSet transaction. If the quorum is zero and signers +// is empty, then any signer list is removed from the account. +STTx getSignerListSetTx ( + UserAccount& acct, SignerList const& signers, std::uint32_t quorum); + +// Return a transaction that creates an un-targeted ticket. +STTx getCreateTicketTx (UserAccount& acct); + +// Return a transaction that creates an targeted ticket. +STTx getCreateTicketTx (UserAccount& acct, UserAccount const& target); + +// Return a transaction that cancels a ticket. +STTx getCancelTicketTx (UserAccount& acct, uint256 const& ticketID); + +// Return an unsigned trust set transaction. +STTx getTrustSetTx (UserAccount& from, Issue const& issuer, int limit); + +//------------------------------------------------------------------------------ + +// Complete the a simple Payment transaction in drops. Expected to succeed. +void payInDrops (TestLedger& ledger, + UserAccount& from, UserAccount const& to, std::uint64_t amountDrops); + +// Return the native balance on an account. +std::uint64_t getNativeBalance(TestLedger& ledger, UserAccount& acct); + +// Return the owner count of an account. +std::uint32_t getOwnerCount(TestLedger& ledger, UserAccount& acct); + +// Get all RippleStates between two accounts. +std::vector getRippleStates ( + TestLedger& ledger, UserAccount const& from, UserAccount const& peer); + +// Get all Offers on an account. +std::vector +getOffersOnAccount (TestLedger& ledger, UserAccount const& acct); + +// Get all Tickets on an account. +std::vector +getTicketsOnAccount (TestLedger& ledger, UserAccount const& acct); + +} // test +} // ripple + +#endif // RIPPLE_APP_TRANSACTORS_TESTS_COMMON_TRANSACTOR_H_INCLUDED diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index 42710d163..fbc14e001 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -659,6 +659,28 @@ private: return rpcError (rpcINVALID_PARAMS); } + // submit any multisigned transaction to the network + // + // submit_multisigned + Json::Value parseSubmitMultiSigned (Json::Value const& jvParams) + { + Json::Value jvRequest; + Json::Reader reader; + bool const bOffline = 2 == jvParams.size () && jvParams[1u].asString () == "offline"; + + if ((1 == jvParams.size () || bOffline) + && reader.parse (jvParams[0u].asString (), jvRequest)) + { + // Multisigned. + if (bOffline) + jvRequest["offline"] = true; + + return jvRequest; + } + + return rpcError (rpcINVALID_PARAMS); + } + // tx Json::Value parseTx (Json::Value const& jvParams) { @@ -859,6 +881,9 @@ public: { "sign_for", &RPCParser::parseSignFor, 4, 4 }, #endif // RIPPLE_ENABLE_MULTI_SIGN { "submit", &RPCParser::parseSignSubmit, 1, 3 }, +#if RIPPLE_ENABLE_MULTI_SIGN + { "submit_multisigned", &RPCParser::parseSubmitMultiSigned, 1, 1 }, +#endif // RIPPLE_ENABLE_MULTI_SIGN { "server_info", &RPCParser::parseAsIs, 0, 0 }, { "server_state", &RPCParser::parseAsIs, 0, 0 }, { "stop", &RPCParser::parseAsIs, 0, 0 }, diff --git a/src/ripple/protocol/STTx.h b/src/ripple/protocol/STTx.h index 6b3df5ce8..c3b616b81 100644 --- a/src/ripple/protocol/STTx.h +++ b/src/ripple/protocol/STTx.h @@ -157,6 +157,9 @@ public: std::string const& escapedMetaData) const; private: + bool checkSingleSign () const; + bool checkMultiSign () const; + TxType tx_type_; mutable boost::tribool sig_state_; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index e858a79f0..dc7f4183f 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -56,7 +56,6 @@ enum TER // aka TransactionEngineResult temMALFORMED = -299, temBAD_AMOUNT, - temBAD_AUTH_MASTER, temBAD_CURRENCY, temBAD_EXPIRATION, temBAD_FEE, @@ -113,6 +112,10 @@ enum TER // aka TransactionEngineResult tefWRONG_PRIOR, tefMASTER_DISABLED, tefMAX_LEDGER, + tefBAD_SIGNATURE, + tefBAD_QUORUM, + tefNOT_MULTI_SIGNING, + tefBAD_AUTH_MASTER, // -99 .. -1: R Retry // sequence too high, no funds for txn fee, originating -account diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp index c3b2a36ff..800d3b8ef 100755 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -28,6 +28,17 @@ InnerObjectFormats::InnerObjectFormats () << SOElement (sfAccount, SOE_REQUIRED) << SOElement (sfSignerWeight, SOE_REQUIRED) ; + + add (sfSigningFor.getJsonName ().c_str (), sfSigningFor.getCode ()) + << SOElement (sfAccount, SOE_REQUIRED) + << SOElement (sfSigningAccounts, SOE_REQUIRED) + ; + + add (sfSigningAccount.getJsonName ().c_str (), sfSigningAccount.getCode ()) + << SOElement (sfAccount, SOE_REQUIRED) + << SOElement (sfSigningPubKey, SOE_REQUIRED) + << SOElement (sfMultiSignature, SOE_REQUIRED) + ; } void InnerObjectFormats::addCommonFields (Item& item) diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index 299a4c543..d927731ea 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -18,17 +18,18 @@ //============================================================================== #include -#include +#include #include #include #include #include #include -#include #include +#include #include #include #include +#include // #include #include @@ -195,15 +196,16 @@ bool STTx::checkSign () const { try { - ECDSA const fullyCanonical = (getFlags() & tfFullyCanonicalSig) - ? ECDSA::strict - : ECDSA::not_strict; - - RippleAddress n; - n.setAccountPublic (getFieldVL (sfSigningPubKey)); - - sig_state_ = n.accountPublicVerify (getSigningData (*this), - getFieldVL (sfTxnSignature), fullyCanonical); +#if RIPPLE_ENABLE_MULTI_SIGN + // Determine whether we're single- or multi-signing by looking + // at the SigningPubKey. It it's empty we must be multi-signing. + // Otherwise we're single-signing. + Blob const& signingPubKey = getFieldVL (sfSigningPubKey); + sig_state_ = signingPubKey.empty () ? + checkMultiSign () : checkSingleSign (); +#else + sig_state_ = checkSingleSign (); +#endif // RIPPLE_ENABLE_MULTI_SIGN } catch (...) { @@ -280,6 +282,217 @@ STTx::getMetaSQL (Serializer rawTxn, % getSequence () % inLedger % status % rTxn % escapedMetaData); } +bool +STTx::checkSingleSign () const +{ + // We don't allow both a non-empty sfSigningPubKey and an sfMultiSigners. + // That would allow the transaction to be signed two ways. So if both + // fields are present the signature is invalid. + if (isFieldPresent (sfMultiSigners)) + return false; + + bool ret = false; + try + { + ECDSA const fullyCanonical = (getFlags() & tfFullyCanonicalSig) + ? ECDSA::strict + : ECDSA::not_strict; + + RippleAddress n; + n.setAccountPublic (getFieldVL (sfSigningPubKey)); + + ret = n.accountPublicVerify (getSigningData (*this), + getFieldVL (sfTxnSignature), fullyCanonical); + } + catch (...) + { + // Assume it was a signature failure. + ret = false; + } + return ret; +} + +bool +STTx::checkMultiSign () const +{ + // Make sure the MultiSigners are present. Otherwise they are not + // attempting multi-signing and we just have a bad SigningPubKey. + if (!isFieldPresent (sfMultiSigners)) + return false; + + STArray const& multiSigners (getFieldArray (sfMultiSigners)); + + // There are well known bounds that the number of signers must be within. + { + std::size_t const multiSignerCount = multiSigners.size (); + if ((multiSignerCount < 1) || (multiSignerCount > maxMultiSigners)) + return false; + } + + // We can ease the computational load inside the loop a bit by + // pre-constructing part of the data that we hash. Fill a Serializer + // with the stuff that stays constant from signature to signature. + Serializer const dataStart (startMultiSigningData ()); + + // We also use the sfAccount field inside the loop. Get it once. + RippleAddress const txnAccountID = getFieldAccount (sfAccount); + + // Determine whether signatures must be full canonical. + ECDSA const fullyCanonical = (getFlags() & tfFullyCanonicalSig) + ? ECDSA::strict + : ECDSA::not_strict; + + // We need to detect (and reject) if a multi-signer is both signing + // directly and using a SigningFor. Here's an example: + // + // { + // ... + // "MultiSigners": [ + // { + // "SigningFor": { + // "Account": "", + // "SigningAccounts": [ + // { + // "SigningAccount": { + // // * becky says that becky signs for alice. * + // "Account": "", + // ... + // "SigningFor": { + // "Account": "", + // "SigningAccounts": [ + // { + // "SigningAccount": { + // // * cheri says that becky signs for alice. * + // "Account": "", + // ... + // "tx_json": { + // "Account": "", + // ... + // } + // } + // + // Why is this way of signing a problem? Alice has a signer list, and + // Becky can show up in that list only once. By design. So if Becky + // signs twice -- once directly and once indirectly -- we have three + // options: + // + // 1. We can add Becky's weight toward Alice's quorum twice, once for + // each signature. This seems both unexpected and counter to Alice's + // intention. + // + // 2. We could allow both signatures, but only add Becky's weight + // toward Alice's quorum once. This seems a bit better. But it allows + // our clients to ask rippled to do more work than necessary. We + // should also let the client know that only one of the signatures + // was necessary. + // + // 3. The only way to tell the client that they have done more work + // than necessary (and that one of the signatures will be ignored) is + // to declare the transaction malformed. This behavior also aligns + // well with rippled's behavior if Becky had signed directly twice: + // the transaction would be marked as malformed. + // + // We use this std::set to detect this form of double-signing. + std::set firstLevelSigners; + + // SigningFors must be in sorted order by AccountID. + RippleAddress lastSigningForID; + lastSigningForID.setAccountID (""); + + // Every signature must verify or we reject the transaction. + for (auto const& signingFor : multiSigners) + { + RippleAddress const signingForID = + signingFor.getFieldAccount (sfAccount); + + // SigningFors must be in order by account ID. No duplicates allowed. + if (lastSigningForID >= signingForID) + return false; + + // The next SigningFor must be greater than this one. + lastSigningForID = signingForID; + + // If signingForID is *not* txnAccountID, then look for duplicates. + bool const directSigning = (signingForID == txnAccountID); + if (! directSigning) + { + if (! firstLevelSigners.insert (signingForID).second) + // This is a duplicate signer. Fail. + return false; + } + + STArray const& signingAccounts ( + signingFor.getFieldArray (sfSigningAccounts)); + + // There are bounds that the number of signers must be within. + { + std::size_t const signingAccountsCount = signingAccounts.size (); + if ((signingAccountsCount < 1) || + (signingAccountsCount > maxMultiSigners)) + { + return false; + } + } + + // SingingAccounts must be in sorted order by AccountID. + RippleAddress lastSigningAcctID; + lastSigningAcctID.setAccountID (""); + + for (auto const& signingAcct : signingAccounts) + { + RippleAddress const signingAcctID = + signingAcct.getFieldAccount (sfAccount); + + // None of the multi-signers may sign for themselves. + if (signingForID == signingAcctID) + return false; + + // Accounts must be in order by account ID. No duplicates allowed. + if (lastSigningAcctID >= signingAcctID) + return false; + + // The next signature must be greater than this one. + lastSigningAcctID = signingAcctID; + + // If signingForID *is* txnAccountID, then look for duplicates. + if (directSigning) + { + if (! firstLevelSigners.insert (signingAcctID).second) + // This is a duplicate signer. Fail. + return false; + } + + // Verify the signature. + bool validSig = false; + try + { + Serializer s = dataStart; + finishMultiSigningData (signingForID, signingAcctID, s); + + RippleAddress const pubKey = + RippleAddress::createAccountPublic ( + signingAcct.getFieldVL (sfSigningPubKey)); + + Blob const signature = + signingAcct.getFieldVL (sfMultiSignature); + + validSig = pubKey.accountPublicVerify ( + s.getData(), signature, fullyCanonical); + } + catch (...) + { + // We assume any problem lies with the signature. + validSig = false; + } + if (!validSig) + return false; + } + } + + // All signatures verified. + return true; +} + //------------------------------------------------------------------------------ static diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index d57364350..70ef4bb42 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -73,6 +73,8 @@ bool transResultInfo (TER code, std::string& token, std::string& text) { tefBAD_ADD_AUTH, "tefBAD_ADD_AUTH", "Not authorized to add account." }, { tefBAD_AUTH, "tefBAD_AUTH", "Transaction's public key is not authorized." }, { tefBAD_LEDGER, "tefBAD_LEDGER", "Ledger in unexpected state." }, + { tefBAD_QUORUM, "tefBAD_QUORUM", "Signatures provided do not meet the quorum." }, + { tefBAD_SIGNATURE, "tefBAD_SIGNATURE", "A signature is provided for a non-signer." }, { tefCREATED, "tefCREATED", "Can't add an already created account." }, { tefEXCEPTION, "tefEXCEPTION", "Unexpected program state." }, { tefFAILURE, "tefFAILURE", "Failed to apply." }, @@ -80,8 +82,10 @@ bool transResultInfo (TER code, std::string& token, std::string& text) { tefMASTER_DISABLED, "tefMASTER_DISABLED", "Master key is disabled." }, { tefMAX_LEDGER, "tefMAX_LEDGER", "Ledger sequence too high." }, { tefNO_AUTH_REQUIRED, "tefNO_AUTH_REQUIRED", "Auth is not required." }, + { tefNOT_MULTI_SIGNING, "tefNOT_MULTI_SIGNING", "Account has no appropriate list of multi-signers." }, { tefPAST_SEQ, "tefPAST_SEQ", "This sequence number has already past." }, { tefWRONG_PRIOR, "tefWRONG_PRIOR", "This previous transaction does not match." }, + { tefBAD_AUTH_MASTER, "tefBAD_AUTH_MASTER", "Auth for unclaimed account needs correct master key." }, { telLOCAL_ERROR, "telLOCAL_ERROR", "Local failure." }, { telBAD_DOMAIN, "telBAD_DOMAIN", "Domain too long." }, @@ -93,7 +97,6 @@ bool transResultInfo (TER code, std::string& token, std::string& text) { temMALFORMED, "temMALFORMED", "Malformed transaction." }, { temBAD_AMOUNT, "temBAD_AMOUNT", "Can only send positive amounts." }, - { temBAD_AUTH_MASTER, "temBAD_AUTH_MASTER", "Auth for unclaimed account needs correct master key." }, { temBAD_CURRENCY, "temBAD_CURRENCY", "Malformed: Bad currency." }, { temBAD_EXPIRATION, "temBAD_EXPIRATION", "Malformed: Bad expiration." }, { temBAD_FEE, "temBAD_FEE", "Invalid fee, negative or not XRP." }, diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 9e42f1f88..260e79d43 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -112,6 +112,7 @@ void TxFormats::addCommonFields (Item& item) << SOElement(sfMemos, SOE_OPTIONAL) << SOElement(sfSigningPubKey, SOE_REQUIRED) << SOElement(sfTxnSignature, SOE_OPTIONAL) + << SOElement(sfMultiSigners, SOE_OPTIONAL) // submit_multisigned ; } diff --git a/src/ripple/protocol/tests/STTx.test.cpp b/src/ripple/protocol/tests/STTx.test.cpp index 4386abfd7..fdf3f4aaf 100644 --- a/src/ripple/protocol/tests/STTx.test.cpp +++ b/src/ripple/protocol/tests/STTx.test.cpp @@ -77,6 +77,134 @@ public: } }; +class InnerObjectFormatsSerializer_test : public beast::unit_test::suite +{ +public: + void run() + { + // Create a transaction + RippleAddress txnSeed; + txnSeed.setSeedRandom (); + RippleAddress txnGenerator = txnSeed.createGeneratorPublic (txnSeed); + RippleAddress txnPublicAcct = txnSeed.createAccountPublic (txnGenerator, 1); + + STTx txn (ttACCOUNT_SET); + txn.setSourceAccount (txnPublicAcct); + txn.setSigningPubKey (txnPublicAcct); + txn.setFieldVL (sfMessageKey, txnPublicAcct.getAccountPublic ()); + Blob const emptyBlob; // Make empty signature for multi-signing + txn.setFieldVL (sfSigningPubKey, emptyBlob); + + // Create fields for a SigningAccount + RippleAddress saSeed; + saSeed.setSeedGeneric ("masterpassphrase"); + RippleAddress const saGenerator = saSeed.createGeneratorPublic (saSeed); + RippleAddress const saPublicAcct = + saSeed.createAccountPublic (saGenerator, 1); + Account const saID = saPublicAcct.getAccountID (); + + // Create a field for SigningFor + Account const signingForID = txnPublicAcct.getAccountID (); + + RippleAddress saPrivateAcct = + saSeed.createAccountPrivate(saGenerator, saSeed, 0); + + // Get the stream of the transaction for use in multi-signing. + Serializer s = txn.getMultiSigningData (saPublicAcct, saPublicAcct); + + Blob saMultiSignature = + saPrivateAcct.accountPrivateSign (s.getData()); + + // The InnerObjectFormats say a SigningAccount is supposed to look + // like this: + // SigningAccount { + // Account: "...", + // MultiSignature: "...", + // PublicKey: "..."" + // } + // Make one well formed SigningAccount and several mal-formed ones. + // See whether the serializer lets the good one through and catches + // the bad ones. + + // This lambda contains the bulk of the test code. + auto testMalformedSigningAccount = + [this, &txn, &signingForID] + (STObject const& signingAcct, bool expectPass) + { + // Create SigningAccounts array. + STArray signingAccts (sfSigningAccounts, 1); + signingAccts.push_back (signingAcct); + + // Insert SigningAccounts array into SigningFor object. + STObject signingFor (sfSigningFor); + signingFor.setFieldAccount (sfAccount, signingForID); + signingFor.setFieldArray (sfSigningAccounts, signingAccts); + + // Insert SigningFor into MultiSigners. + STArray multiSigners (sfMultiSigners, 1); + multiSigners.push_back (signingFor); + + // Insert MultiSigners into transaction. + STTx tempTxn (txn); + tempTxn.setFieldArray (sfMultiSigners, multiSigners); + + Serializer rawTxn; + tempTxn.add (rawTxn); + SerialIter sit (rawTxn); + bool serialized = false; + try + { + STTx copy (sit); + serialized = true; + } + catch (...) + { + ; // If it threw then serialization failed. + } + expect (serialized == expectPass, + "Unexpected serialized = " + std::to_string (serialized) + + ". Object:\n" + signingAcct.getFullText () + "\n"); + }; + + { + // Test case 1. Make a valid SigningAccount object. + STObject soTest1 (sfSigningAccount); + soTest1.setFieldAccount (sfAccount, saID); + soTest1.setFieldVL (sfSigningPubKey, + txnPublicAcct.getAccountPublic ()); + soTest1.setFieldVL (sfMultiSignature, saMultiSignature); + testMalformedSigningAccount (soTest1, true); + } + { + // Test case 2. Omit sfSigningPubKey from SigningAccount. + STObject soTest2 (sfSigningAccount); + soTest2.setFieldAccount (sfAccount, saID); + soTest2.setFieldVL (sfMultiSignature, saMultiSignature); + testMalformedSigningAccount (soTest2, false); + } + { + // Test case 3. Extra sfAmount in SigningAccount. + STObject soTest3 (sfSigningAccount); + soTest3.setFieldAccount (sfAccount, saID); + soTest3.setFieldVL (sfSigningPubKey, + txnPublicAcct.getAccountPublic ()); + soTest3.setFieldVL (sfMultiSignature, saMultiSignature); + soTest3.setFieldAmount (sfAmount, STAmount (10000)); + testMalformedSigningAccount (soTest3, false); + } + { + // Test case 4. Right number of fields, but wrong ones. + STObject soTest4 (sfSigningAccount); + soTest4.setFieldVL (sfSigningPubKey, + txnPublicAcct.getAccountPublic ()); + soTest4.setFieldVL (sfMultiSignature, saMultiSignature); + soTest4.setFieldAmount (sfAmount, STAmount (10000)); + testMalformedSigningAccount (soTest4, false); + } + } +}; + BEAST_DEFINE_TESTSUITE(STTx,ripple_app,ripple); +BEAST_DEFINE_TESTSUITE(InnerObjectFormatsSerializer,ripple_app,ripple); } // ripple diff --git a/src/ripple/rpc/handlers/SubmitMultiSigned.cpp b/src/ripple/rpc/handlers/SubmitMultiSigned.cpp new file mode 100644 index 000000000..55f706f29 --- /dev/null +++ b/src/ripple/rpc/handlers/SubmitMultiSigned.cpp @@ -0,0 +1,39 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2014 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 +#include + +namespace ripple { + +// { +// SigningAccounts , +// tx_json: , +// } +Json::Value doSubmitMultiSigned (RPC::Context& context) +{ + context.loadType = Resource::feeHighBurdenRPC; + + NetworkOPs::FailHard const failType = NetworkOPs::doFailHard ( + context.params.isMember ("fail_hard") + && context.params["fail_hard"].asBool ()); + + return RPC::transactionSubmitMultiSigned ( + context.params, failType, context.netOps, context.role); +} + +} // ripple diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index 63018b087..019f1c2d9 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -137,6 +137,9 @@ HandlerTable HANDLERS({ { "sign_for", byRef (&doSignFor), Role::USER, NO_CONDITION }, #endif // RIPPLE_ENABLE_MULTI_SIGN { "submit", byRef (&doSubmit), Role::USER, NEEDS_CURRENT_LEDGER }, +#if RIPPLE_ENABLE_MULTI_SIGN + { "submit_multisigned", byRef (&doSubmitMultiSigned), Role::USER, NEEDS_CURRENT_LEDGER }, +#endif // RIPPLE_ENABLE_MULTI_SIGN { "server_info", byRef (&doServerInfo), Role::USER, NO_CONDITION }, { "server_state", byRef (&doServerState), Role::USER, NO_CONDITION }, { "stop", byRef (&doStop), Role::ADMIN, NO_CONDITION }, diff --git a/src/ripple/rpc/impl/TransactionSign.cpp b/src/ripple/rpc/impl/TransactionSign.cpp index d998bedb2..0e2765ff9 100644 --- a/src/ripple/rpc/impl/TransactionSign.cpp +++ b/src/ripple/rpc/impl/TransactionSign.cpp @@ -525,7 +525,6 @@ struct transactionPreProcessResult transactionPreProcessResult& operator= (transactionPreProcessResult&&) = delete; - transactionPreProcessResult (Json::Value&& json) : first (std::move (json)) , second () @@ -1019,5 +1018,276 @@ Json::Value transactionSignFor ( return json; } +/** Returns a Json::objectValue. */ +Json::Value transactionSubmitMultiSigned ( + Json::Value jvRequest, + NetworkOPs::FailHard failType, + detail::TxnSignApiFacade& apiFacade, + Role role) +{ + WriteLog (lsDEBUG, RPCHandler) + << "transactionSubmitMultiSigned: " << jvRequest; + + // When multi-signing, the "Sequence" and "SigningPubKey" fields must + // be passed in by the caller. + using namespace detail; + { + Json::Value err = checkMultiSignFields (jvRequest); + if (RPC::contains_error (err)) + return std::move (err); + } + + Json::Value& tx_json (jvRequest ["tx_json"]); + + auto const txJsonResult = checkTxJsonFields(tx_json, apiFacade, role, true); + if (RPC::contains_error (txJsonResult.first)) + return std::move (txJsonResult.first); + + RippleAddress const raSrcAddressID = std::move (txJsonResult.second); + + apiFacade.snapshotAccountState (raSrcAddressID); + if (!apiFacade.isValidAccount ()) + { + // If not offline and did not find account, error. + WriteLog (lsDEBUG, RPCHandler) + << "transactionSubmitMultiSigned: Failed to find source account " + << "in current ledger: " + << raSrcAddressID.humanAccountID (); + + return rpcError (rpcSRC_ACT_NOT_FOUND); + } + + { + Json::Value err = checkFee (jvRequest, apiFacade, role, AutoFill::dont); + if (RPC::contains_error(err)) + return std::move (err); + + err = checkPayment ( + jvRequest, + tx_json, + raSrcAddressID, + apiFacade, + role, + PathFind::dont); + + if (RPC::contains_error(err)) + return std::move (err); + } + + // Grind through the JSON in tx_json to produce a STTx + STTx::pointer stpTrans; + { + STParsedJSONObject parsedTx_json ("tx_json", tx_json); + if (!parsedTx_json.object) + { + Json::Value jvResult; + jvResult ["error"] = parsedTx_json.error ["error"]; + jvResult ["error_code"] = parsedTx_json.error ["error_code"]; + jvResult ["error_message"] = parsedTx_json.error ["error_message"]; + return jvResult; + } + try + { + stpTrans = + std::make_shared (std::move(parsedTx_json.object.get())); + } + catch (std::exception& ex) + { + std::string reason (ex.what ()); + return RPC::make_error (rpcINTERNAL, + "Exception while serializing transaction: " + reason); + } + std::string reason; + if (!passesLocalChecks (*stpTrans, reason)) + return RPC::make_error (rpcINVALID_PARAMS, reason); + } + + // Validate the fields in the serialized transaction. + { + // We now have the transaction text serialized and in the right format. + // Verify the values of select fields. + // + // The SigningPubKey must be present but empty. + if (!stpTrans->getFieldVL (sfSigningPubKey).empty ()) + { + std::ostringstream err; + err << "Invalid " << sfSigningPubKey.fieldName + << " field. Field must be empty when multi-signing."; + return RPC::make_error (rpcINVALID_PARAMS, err.str ()); + } + + // The Fee field must be greater than zero. + if (stpTrans->getFieldAmount (sfFee) <= 0) + { + std::ostringstream err; + err << "Invalid " << sfFee.fieldName + << " field. Value must be greater than zero."; + return RPC::make_error (rpcINVALID_PARAMS, err.str ()); + } + } + + // Check MultiSigners for valid entries. + const char* multiSignersArrayName {sfMultiSigners.getJsonName ().c_str ()}; + + STArray multiSigners; + { + // Verify that the MultiSigners field is present and an array. + if (! jvRequest.isMember (multiSignersArrayName)) + return RPC::missing_field_error (multiSignersArrayName); + + Json::Value& multiSignersValue ( + jvRequest [multiSignersArrayName]); + + if (! multiSignersValue.isArray ()) + { + std::ostringstream err; + err << "Expected " + << multiSignersArrayName << " to be an array"; + return RPC::make_param_error (err.str ()); + } + + // Convert the MultiSigners into SerializedTypes. + STParsedJSONArray parsedMultiSigners ( + multiSignersArrayName, multiSignersValue); + + if (!parsedMultiSigners.array) + { + Json::Value jvResult; + jvResult ["error"] = parsedMultiSigners.error ["error"]; + jvResult ["error_code"] = + parsedMultiSigners.error ["error_code"]; + jvResult ["error_message"] = + parsedMultiSigners.error ["error_message"]; + return jvResult; + } + multiSigners = std::move (parsedMultiSigners.array.get()); + } + + if (multiSigners.empty ()) + return RPC::make_param_error ("MultiSigners array may not be empty."); + + for (STObject const& signingFor : multiSigners) + { + if (signingFor.getFName () != sfSigningFor) + return RPC::make_param_error ( + "MultiSigners array has a non-SigningFor entry"); + + // Each SigningAccounts array must have at least one entry. + STArray const& signingAccounts = + signingFor.getFieldArray (sfSigningAccounts); + + if (signingAccounts.empty ()) + return RPC::make_param_error ( + "A SigningAccounts array may not be empty"); + + // Each SigningAccounts array may only contain SigningAccount objects. + auto const signingAccountsEnd = signingAccounts.end (); + auto const findItr = std::find_if ( + signingAccounts.begin (), signingAccountsEnd, + [](STObject const& obj) + { return obj.getFName () != sfSigningAccount; }); + + if (findItr != signingAccountsEnd) + return RPC::make_param_error ( + "SigningAccounts may only contain SigningAccount objects."); + } + + // Lambdas for sorting arrays and finding duplicates. + auto byFieldAccountID = + [] (STObject const& a, STObject const& b) { + return (a.getFieldAccount (sfAccount).getAccountID () < + b.getFieldAccount (sfAccount).getAccountID ()); }; + + auto ifDuplicateAccountID = + [] (STObject const& a, STObject const& b) { + return (a.getFieldAccount (sfAccount).getAccountID () == + b.getFieldAccount (sfAccount).getAccountID ()); }; + + { + // MultiSigners are submitted sorted in Account order. This + // assures that the same list will always have the same hash. + multiSigners.sort (byFieldAccountID); + + // There may be no duplicate Accounts in MultiSigners + auto const multiSignersEnd = multiSigners.end (); + + auto const dupAccountItr = std::adjacent_find ( + multiSigners.begin (), multiSignersEnd, ifDuplicateAccountID); + + if (dupAccountItr != multiSignersEnd) + { + std::ostringstream err; + err << "Duplicate SigningFor:Account entries (" + << dupAccountItr->getFieldAccount (sfAccount).humanAccountID () + << ") are not allowed."; + return RPC::make_param_error(err.str ()); + } + } + + // All SigningAccounts inside the MultiSigners must also be sorted and + // contain no duplicates. + for (STObject& signingFor : multiSigners) + { + STArray& signingAccts = signingFor.peekFieldArray (sfSigningAccounts); + signingAccts.sort (byFieldAccountID); + + auto const signingAcctsEnd = signingAccts.end (); + + auto const dupAccountItr = std::adjacent_find ( + signingAccts.begin (), signingAcctsEnd, ifDuplicateAccountID); + + if (dupAccountItr != signingAcctsEnd) + { + std::ostringstream err; + err << "Duplicate SigningAccounts:SigningAccount:Account entries (" + << dupAccountItr->getFieldAccount (sfAccount).humanAccountID () + << ") are not allowed."; + return RPC::make_param_error(err.str ()); + } + + // An account may not sign for itself. + RippleAddress const signingForAcct = + signingFor.getFieldAccount (sfAccount); + + auto const selfSigningItr = std::find_if( + signingAccts.begin (), signingAcctsEnd, + [&signingForAcct](STObject const& elem) + { return elem.getFieldAccount (sfAccount) == signingForAcct; }); + + if (selfSigningItr != signingAcctsEnd) + { + std::ostringstream err; + err << "A SigningAccount may not SignFor itself (" + << signingForAcct.humanAccountID () << ")."; + return RPC::make_param_error(err.str ()); + } + } + + // Insert the MultiSigners into the transaction. + stpTrans->setFieldArray (sfMultiSigners, std::move(multiSigners)); + + // Make sure the SerializedTransaction makes a legitimate Transaction. + std::pair txn = + transactionConstructImpl (stpTrans); + + if (!txn.second) + return txn.first; + + // Finally, submit the transaction. + try + { + // FIXME: For performance, should use asynch interface + apiFacade.processTransaction ( + txn.second, role == Role::ADMIN, true, failType); + } + catch (std::exception&) + { + return RPC::make_error (rpcINTERNAL, + "Exception occurred during transaction submission."); + } + + return transactionFormatResultImpl (txn.second); +} + } // RPC } // ripple diff --git a/src/ripple/rpc/impl/TransactionSign.h b/src/ripple/rpc/impl/TransactionSign.h index 729949bf5..496c58484 100644 --- a/src/ripple/rpc/impl/TransactionSign.h +++ b/src/ripple/rpc/impl/TransactionSign.h @@ -168,6 +168,24 @@ Json::Value transactionSignFor ( return transactionSignFor (params, failType, apiFacade, role); } +/** Returns a Json::objectValue. */ +Json::Value transactionSubmitMultiSigned ( + Json::Value params, // Passed by value so it can be modified locally. + NetworkOPs::FailHard failType, + detail::TxnSignApiFacade& apiFacade, + Role role); + +/** Returns a Json::objectValue. */ +Json::Value transactionSubmitMultiSigned ( + Json::Value const& params, + NetworkOPs::FailHard failType, + NetworkOPs& netOPs, + Role role) +{ + detail::TxnSignApiFacade apiFacade (netOPs); + return transactionSubmitMultiSigned (params, failType, apiFacade, role); +} + } // RPC } // ripple diff --git a/src/ripple/rpc/tests/JSONRPC.test.cpp b/src/ripple/rpc/tests/JSONRPC.test.cpp index 11d7ce54b..5bd50494d 100644 --- a/src/ripple/rpc/tests/JSONRPC.test.cpp +++ b/src/ripple/rpc/tests/JSONRPC.test.cpp @@ -963,7 +963,8 @@ public: { TestStuff {transactionSign, "sign", 0}, TestStuff {transactionSubmit, "submit", 1}, - TestStuff {transactionSignFor, "sign_for", 2} + TestStuff {transactionSignFor, "sign_for", 2}, + TestStuff {transactionSubmitMultiSigned, "submit_multisigned", 3} }; for (auto testFunc : testFuncs) diff --git a/src/ripple/unity/app4.cpp b/src/ripple/unity/app4.cpp index c998bf736..bddecf949 100644 --- a/src/ripple/unity/app4.cpp +++ b/src/ripple/unity/app4.cpp @@ -47,5 +47,7 @@ #include #include +#include +#include #include #include diff --git a/src/ripple/unity/rpcx.cpp b/src/ripple/unity/rpcx.cpp index 6b9091b93..ec30fc67b 100644 --- a/src/ripple/unity/rpcx.cpp +++ b/src/ripple/unity/rpcx.cpp @@ -76,6 +76,7 @@ #include #include #include +#include #include #include #include