From 64ebd64d2b1176d6656a4075ea57bff10097d207 Mon Sep 17 00:00:00 2001 From: Scott Schurr Date: Fri, 6 Feb 2015 10:22:36 -0800 Subject: [PATCH] SignerListSet txn and InnerObjectFormats (RIPD-182): Add support for the SignerListSet transaction as a step toward multi-sign support. As part of the SignerListSet implementation, add InnerObjectFormat templates (similar to TxFormats and LedgerFormats) and enforce them in STObject, STArray, and STParsedJSON. --- Builds/VisualStudio2013/RippleD.vcxproj | 28 +- .../VisualStudio2013/RippleD.vcxproj.filters | 21 +- SConstruct | 1 - src/BeastConfig.h | 9 + src/ripple/app/ledger/LedgerEntrySet.cpp | 63 +-- src/ripple/app/ledger/LedgerEntrySet.h | 2 + src/ripple/app/main/Main.cpp | 2 + src/ripple/app/transactors/README.md | 90 ----- src/ripple/app/tx/README.md | 217 +++++++++++ src/ripple/app/tx/impl/SetSignerList.cpp | 363 ++++++++++++++++++ src/ripple/app/tx/impl/SignerEntries.cpp | 67 ++++ src/ripple/app/tx/impl/SignerEntries.h | 77 ++++ src/ripple/app/tx/impl/Transactor.cpp | 26 +- src/ripple/app/tx/impl/Transactor.h | 5 + src/ripple/protocol/Indexes.h | 2 + .../InnerObjectFormats.h} | 29 +- src/ripple/protocol/LedgerFormats.h | 3 + src/ripple/protocol/SField.h | 5 +- src/ripple/protocol/STArray.h | 6 +- src/ripple/protocol/STObject.h | 11 + src/ripple/protocol/STTx.h | 2 + src/ripple/protocol/TER.h | 2 + src/ripple/protocol/TxFormats.h | 1 + src/ripple/protocol/impl/Indexes.cpp | 11 + .../protocol/impl/InnerObjectFormats.cpp | 55 +++ src/ripple/protocol/impl/LedgerFormats.cpp | 8 + src/ripple/protocol/impl/SField.cpp | 5 +- src/ripple/protocol/impl/STArray.cpp | 5 + src/ripple/protocol/impl/STObject.cpp | 28 +- src/ripple/protocol/impl/STParsedJSON.cpp | 38 +- src/ripple/protocol/impl/TER.cpp | 2 + src/ripple/protocol/impl/TxFormats.cpp | 7 + .../tests/InnerObjectFormats.test.cpp | 204 ++++++++++ src/ripple/rpc/handlers/AccountInfo.cpp | 25 +- src/ripple/unity/app4.cpp | 3 + src/ripple/unity/protocol.cpp | 2 + 36 files changed, 1290 insertions(+), 135 deletions(-) delete mode 100644 src/ripple/app/transactors/README.md create mode 100644 src/ripple/app/tx/README.md create mode 100644 src/ripple/app/tx/impl/SetSignerList.cpp create mode 100644 src/ripple/app/tx/impl/SignerEntries.cpp create mode 100644 src/ripple/app/tx/impl/SignerEntries.h rename src/ripple/{unity/app9.cpp => protocol/InnerObjectFormats.h} (62%) mode change 100644 => 100755 create mode 100755 src/ripple/protocol/impl/InnerObjectFormats.cpp create mode 100755 src/ripple/protocol/tests/InnerObjectFormats.test.cpp diff --git a/Builds/VisualStudio2013/RippleD.vcxproj b/Builds/VisualStudio2013/RippleD.vcxproj index 43a0be259..56b366881 100644 --- a/Builds/VisualStudio2013/RippleD.vcxproj +++ b/Builds/VisualStudio2013/RippleD.vcxproj @@ -1976,12 +1976,26 @@ ..\..\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 ..\..\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 @@ -2794,6 +2808,10 @@ True True + + True + True + True True @@ -2894,6 +2912,8 @@ + + @@ -2960,6 +2980,10 @@ True True + + True + True + True True @@ -3499,10 +3523,6 @@ True True - - True - True - True True diff --git a/Builds/VisualStudio2013/RippleD.vcxproj.filters b/Builds/VisualStudio2013/RippleD.vcxproj.filters index fb170d94a..74f5d0db8 100644 --- a/Builds/VisualStudio2013/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2013/RippleD.vcxproj.filters @@ -2529,9 +2529,18 @@ ripple\app\tx\impl + + ripple\app\tx\impl + ripple\app\tx\impl + + ripple\app\tx\impl + + + ripple\app\tx\impl + ripple\app\tx\impl @@ -3327,6 +3336,9 @@ ripple\protocol\impl + + ripple\protocol\impl + ripple\protocol\impl @@ -3405,6 +3417,9 @@ ripple\protocol + + ripple\protocol + ripple\protocol @@ -3501,6 +3516,9 @@ ripple\protocol\tests + + ripple\protocol\tests + ripple\protocol\tests @@ -4068,9 +4086,6 @@ ripple\unity - - ripple\unity - ripple\unity diff --git a/SConstruct b/SConstruct index de6c2b008..adf5d5ce0 100644 --- a/SConstruct +++ b/SConstruct @@ -678,7 +678,6 @@ def get_unity_sources(): 'src/ripple/unity/app6.cpp', 'src/ripple/unity/app7.cpp', 'src/ripple/unity/app8.cpp', - 'src/ripple/unity/app9.cpp', 'src/ripple/unity/core.cpp', 'src/ripple/unity/basics.cpp', 'src/ripple/unity/crypto.cpp', diff --git a/src/BeastConfig.h b/src/BeastConfig.h index 15ad99372..f70696390 100644 --- a/src/BeastConfig.h +++ b/src/BeastConfig.h @@ -202,4 +202,13 @@ #define RIPPLE_ENABLE_TICKETS 0 #endif +/** Config: RIPPLE_ENABLE_MULTI_SIGN + When set, activates the current state of the multi-sign feature which is + under development. When the feature is complete and released this + #define should be removed. +*/ +#ifndef RIPPLE_ENABLE_MULTI_SIGN +#define RIPPLE_ENABLE_MULTI_SIGN 0 +#endif + #endif diff --git a/src/ripple/app/ledger/LedgerEntrySet.cpp b/src/ripple/app/ledger/LedgerEntrySet.cpp index d71b6c6b3..f58cda028 100644 --- a/src/ripple/app/ledger/LedgerEntrySet.cpp +++ b/src/ripple/app/ledger/LedgerEntrySet.cpp @@ -1018,50 +1018,65 @@ uint256 LedgerEntrySet::getNextLedgerIndex ( void LedgerEntrySet::incrementOwnerCount (SLE::ref sleAccount) { - assert (sleAccount); - - std::uint32_t const current_count = sleAccount->getFieldU32 (sfOwnerCount); - - if (current_count == std::numeric_limits::max ()) - { - WriteLog (lsFATAL, LedgerEntrySet) << - "Account " << sleAccount->getFieldAccount160 (sfAccount) << - " owner count exceeds max!"; - return; - } - - sleAccount->setFieldU32 (sfOwnerCount, current_count + 1); - entryModify (sleAccount); + increaseOwnerCount (sleAccount, 1); } void LedgerEntrySet::incrementOwnerCount (Account const& owner) { - incrementOwnerCount(entryCache (ltACCOUNT_ROOT, - getAccountRootIndex (owner))); + increaseOwnerCount( + entryCache (ltACCOUNT_ROOT, getAccountRootIndex (owner)), 1); } -void LedgerEntrySet::decrementOwnerCount (SLE::ref sleAccount) +void +LedgerEntrySet::increaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch) { assert (sleAccount); std::uint32_t const current_count = sleAccount->getFieldU32 (sfOwnerCount); + std::uint32_t new_count = current_count + howMuch; - if (current_count == 0) + // Check for integer overflow -- well defined behavior on unsigned. + if (new_count < current_count) { WriteLog (lsFATAL, LedgerEntrySet) << "Account " << sleAccount->getFieldAccount160 (sfAccount) << - " owner count is already 0!"; - return; + " owner count exceeds max!"; + new_count = std::numeric_limits::max (); } - - sleAccount->setFieldU32 (sfOwnerCount, current_count - 1); + sleAccount->setFieldU32 (sfOwnerCount, new_count); entryModify (sleAccount); } +void LedgerEntrySet::decrementOwnerCount (SLE::ref sleAccount) +{ + decreaseOwnerCount (sleAccount, 1); +} + void LedgerEntrySet::decrementOwnerCount (Account const& owner) { - decrementOwnerCount(entryCache (ltACCOUNT_ROOT, - getAccountRootIndex (owner))); + decreaseOwnerCount( + entryCache (ltACCOUNT_ROOT, getAccountRootIndex (owner)), 1); +} + +void +LedgerEntrySet::decreaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch) +{ + assert (sleAccount); + + std::uint32_t const current_count = sleAccount->getFieldU32 (sfOwnerCount); + std::uint32_t new_count = current_count - howMuch; + + // Check for integer underflow -- well defined behavior on unsigned. + if (new_count > current_count) + { + WriteLog (lsFATAL, LedgerEntrySet) << + "Account " << sleAccount->getFieldAccount160 (sfAccount) << + " owner count set below 0!"; + new_count = 0; + assert (false); // "This is a dangerous place." Stop in a debug build. + } + sleAccount->setFieldU32 (sfOwnerCount, new_count); + entryModify (sleAccount); } TER LedgerEntrySet::offerDelete (SLE::pointer sleOffer) diff --git a/src/ripple/app/ledger/LedgerEntrySet.h b/src/ripple/app/ledger/LedgerEntrySet.h index a3345c8ec..cdc30082b 100644 --- a/src/ripple/app/ledger/LedgerEntrySet.h +++ b/src/ripple/app/ledger/LedgerEntrySet.h @@ -185,11 +185,13 @@ public: /** @{ */ void incrementOwnerCount (SLE::ref sleAccount); void incrementOwnerCount (Account const& owner); + void increaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch); /** @} */ /** @{ */ void decrementOwnerCount (SLE::ref sleAccount); void decrementOwnerCount (Account const& owner); + void decreaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch); /** @} */ // Offer functions. diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index 8d583a3ef..f7b4b10db 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -137,7 +137,9 @@ void printHelp (const po::options_description& desc) " ripple_path_find []\n" " version\n" " server_info\n" + " sign\n" " stop\n" + " submit\n" " tx \n" " unl_add | []\n" " unl_delete |\n" diff --git a/src/ripple/app/transactors/README.md b/src/ripple/app/transactors/README.md deleted file mode 100644 index acacba09d..000000000 --- a/src/ripple/app/transactors/README.md +++ /dev/null @@ -1,90 +0,0 @@ - -# Transactors # - -## Introduction ## - -Each separate kind of transaction is handled by it's own Transactor. -The Transactor base class provides functionality that is common to -all derived Transactors. The Transactor base class also gives derived -classes the ability to replace portions of the default implementation. - - -# Details on Specific Transactors # - -## AddWallet ## - -## Change ## - -## Offers ## - -### CreateOffer ### - -### CancelOffer ### - -## Payment ## - -## SetAccount ## - -## SetRegularKey ## - -## SetTrust ## - -## 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 -authorizes the transaction, which means that the sequence number would have -to be decided at the time of transaction issuance. This would mean that -multi-signature transactions could only be processed in strict "first-in, -first-out" order which is not practical. - -Tickets can be used in lieu of sequence number. A ticket is a special token -which, through a transaction, can be issued by any account and can be -configured with an optional expiry date and an optional associated account. - -### Specifics ### - -The expiry date can be used to constrain the validity of the ticket. If -specified, the ticket will be considered invalid and unusable if the closing -time of the last *validated* ledger is greater than or equal to the expiration -time of the ticket. - -The associated account can be used to specify an account, other than the -issuing account, that is allowed to "consume" the ticket. Consuming a ticket -means to use the ticket in a transaction that is accepted by the network and -makes its way into a validated ledger. If not present, the ticket can only be -consumed by the issuing account. - -*Corner Case:* It is possible that two or more transactions reference the same -ticket and that both go into the same consensus set. During final application -of transactions from the consensus set at most one of these transactions may -succeed; others must fail with the indication that the ticket has been consumed. - -*Reserve:* While a ticket is outstanding, it should count against the reserve -of the *issuer*. - -##### Issuance -We should decide whether, in the case of multi-signature accounts, any single -authorized signer can issue a ticket on the multi-signature accounts' behalf. -This approach has both advantages and disadvantages. - -Advantages include: - -+ Simpler logic for tickets and reduced data - no need to store or consider an associated account. -+ Owner reserves for issued tickets count against the multi-signature account instead of the account of the signer proposing a transaction. -+ Cleaner meta-data: easier to follow who issued a ticket and how many tickets are outstanding and associated with a particular account. - -Disadvantages are: - -+ Any single authorized signer can issue multiple tickets, each counting against the account's reserve. -+ Special-case logic for authorizing tickets on multi-sign accounts. - -I believe that the disadvantages outweigh the advantages, but debate is welcome. - - -### CreateTicket ### - -### CancelTicket ### diff --git a/src/ripple/app/tx/README.md b/src/ripple/app/tx/README.md new file mode 100644 index 000000000..568feabeb --- /dev/null +++ b/src/ripple/app/tx/README.md @@ -0,0 +1,217 @@ + +# Transactors # + +## Introduction ## + +Each separate kind of transaction is handled by it's own Transactor. +The Transactor base class provides functionality that is common to +all derived Transactors. The Transactor base class also gives derived +classes the ability to replace portions of the default implementation. + + +# Details on Specific Transactors # + +## AddWallet ## + +## Change ## + +## Offers ## + +### CreateOffer ### + +### CancelOffer ### + +## Payment ## + +## SetAccount ## + +## SetRegularKey ## + +## 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. + +Transactions on an account which is designated as multi-sign can be authorized +either by using the master or regular keys (unless those are disabled) or by +being signed by a certain number (a quorum) of pre-authorized accounts. + +Some technical details, including tables indicating some of the Ripple +commands and ledger entries to be used for implementing multi-signature, are +currently listed on the [wiki](https://ripple.com/wiki/Multisign) but will +eventually be migrated into this document as well. + +For accounts which are designated as multi-sign, there must be a list which +specifies which accounts are authorized to sign and the quorum threshold. + + - Each authorized account has a weight. The sum of the weights of the signers is used to determine whether a given set of signatures is sufficient for a quorum. + + - The quorum threshold indicates the minimum required weight that the sum of the weights of all signatures must have before a transaction can be authorized. + +### Verification of Multiple Signatures During TX Processing +The current approach to adding multi-signature support is to require that a +transaction is to be signed outside the Ripple network and only submitted +after the quorum has been reached. + +This reduces the implementation footprint and the load imposed on the network, +and mirrors the way transaction signing is currently handled. It will require +some messaging mechanism outside the Ripple network to disseminate the proposed +transaction to the authorized signers and to allow them to apply signatures. + +Supporting in-ledger voting should be considered, but it has both advantages +and disadvantages. + +One of the advantages is increased transparency - transactions are visible as +are the "votes" (the authorized accounts signing the transaction) on the +ledger. However, transactions may also languish for a long time in the ledger, +never reaching a quorum and consuming network resources. + +### Signature Format +We should not develop a new format for multi-sign signatures. Instead every +signer should extract and sign the transaction as they normally would if this +were a regular transaction. The resulting signature will be stored as a triple +of { signing-account, signer-public-key, signature } in an array of signatures +associated with this transaction. + +The advantage of this is that we can reuse the existing signing and +verification code, and leverage the work that will go towards implementing +support for the Ed25519 elliptic curve. + +### Fees ### +Multi-signature transactions impose a heavier load on the network and should +claim higher fees. + +The fee structure is not yet decided, but an escalating fee structure is laid +out and discussed on the [wiki](https://ripple.com/wiki/Multisign). This +might need to be revisited and designed in light of discussions about changing +how fees are handled. + +### Proposed Transaction Cancellation ### +A transaction that has been proposed against a multi-sign account using a +ticket can be positively canceled if a quorum of authorized signers sign and +issue a transaction that consumes that ticket. + +### Implementation ### + +Any account can have one SignerList attached to it. A SignerList contains the +following elements: + + - A list of from 2 to a protocol-defined maximum of 8 signers. Each signer in the array consists of: + - The signer's 160-bit account ID and + - The signer's 16-bit weight (used to calculate whether a quorum is met). + - And, for the entire list, a single 32-bit quorum value. + +Giving the signers different weights allows an account to organize signers so +some are more important than others. A signer with a larger weight has more +significance in achieving the quorum. + +A multi-signed transaction is validated like this: + + - Each signer of the transaction has their signature validated. + - The weights of all valid signers are summed. + - If the sum of the weights equals or exceeds the quorum value then the entire transaction is considered signed. If the sum is below the quorum, then the signature fails with a tefBAD_QUORUM. + + +By making the signer weights 16 bits and the quorum value 32 bits we avoid +concerns about overflows and still have plenty of resolution. + +This transactor allows two operations: + + - Create (or replace) a signer list for the target account. + - Remove any signer list from the target account. + +The data for a transaction creating or replacing a signer list has this +general form: + + { + "TransactionType": "SignerListSet", + "Account": "rDg53Haik2475DJx8bjMDSDPj4VX7htaMd", + "SignerQuorum": 7, + "SignerEntries": [ + { + "SignerEntry": { + "Account": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "SignerWeight": 4 + } + }, + { + "SignerEntry": { + "Account": "rPcNzota6B8YBokhYtcTNqQVCngtbnWfux", + "SignerWeight": 3 + } + } + ] + } + +The data for a transaction that removes any signer list has this form: + + { + "TransactionType": "SignerListSet", + "Account": "rDg53Haik2475DJx8bjMDSDPj4VX7htaMd", + "SignerQuorum": 0 + } + +## SetTrust ## + +## 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 +authorizes the transaction, which means that the sequence number would have +to be decided at the time of transaction issuance. This would mean that +multi-signature transactions could only be processed in strict "first-in, +first-out" order which is not practical. + +Tickets can be used in lieu of sequence number. A ticket is a special token +which, through a transaction, can be issued by any account and can be +configured with an optional expiry date and an optional associated account. + +### Specifics ### + +The expiry date can be used to constrain the validity of the ticket. If +specified, the ticket will be considered invalid and unusable if the closing +time of the last *validated* ledger is greater than or equal to the expiration +time of the ticket. + +The associated account can be used to specify an account, other than the +issuing account, that is allowed to "consume" the ticket. Consuming a ticket +means to use the ticket in a transaction that is accepted by the network and +makes its way into a validated ledger. If not present, the ticket can only be +consumed by the issuing account. + +*Corner Case:* It is possible that two or more transactions reference the same +ticket and that both go into the same consensus set. During final application +of transactions from the consensus set at most one of these transactions may +succeed; others must fail with the indication that the ticket has been consumed. + +*Reserve:* While a ticket is outstanding, it should count against the reserve +of the *issuer*. + +##### Issuance +We should decide whether, in the case of multi-signature accounts, any single +authorized signer can issue a ticket on the multi-signature accounts' behalf. +This approach has both advantages and disadvantages. + +Advantages include: + ++ Simpler logic for tickets and reduced data - no need to store or consider an associated account. ++ Owner reserves for issued tickets count against the multi-signature account instead of the account of the signer proposing a transaction. ++ Cleaner meta-data: easier to follow who issued a ticket and how many tickets are outstanding and associated with a particular account. + +Disadvantages are: + ++ Any single authorized signer can issue multiple tickets, each counting against the account's reserve. ++ Special-case logic for authorizing tickets on multi-sign accounts. + +I believe that the disadvantages outweigh the advantages, but debate is welcome. + + +### CreateTicket ### + +### CancelTicket ### diff --git a/src/ripple/app/tx/impl/SetSignerList.cpp b/src/ripple/app/tx/impl/SetSignerList.cpp new file mode 100644 index 000000000..9d893ea60 --- /dev/null +++ b/src/ripple/app/tx/impl/SetSignerList.cpp @@ -0,0 +1,363 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +/** +See the README.md for an overview of the SetSignerList transaction that +this class implements. +*/ +class SetSignerList final + : public Transactor +{ +private: + // Values determined during preCheck for use later. + enum Operation {unknown, set, destroy}; + Operation do_ {unknown}; + std::uint32_t quorum_ {0}; + std::vector signers_; + +public: + SetSignerList ( + STTx const& txn, + TransactionEngineParams params, + TransactionEngine* engine) + : Transactor ( + txn, + params, + engine, + deprecatedLogs().journal("SetSignerList")) + { + + } + + /** + Applies the transaction if it is well formed and the ledger state permits. + */ + TER doApply () override; + +protected: + /** + Check anything that can be checked without the ledger. + */ + TER preCheck () override; + +private: + // signers are not const because method (intentionally) sorts vector. + TER validateQuorumAndSignerEntries ( + std::uint32_t quorum, + std::vector& signers) const; + + // Methods called by doApply() + TER replaceSignerList (uint256 const& index); + TER destroySignerList (uint256 const& index); + + void writeSignersToLedger (SLE::pointer ledgerEntry); + + static std::size_t ownerCountDelta (std::size_t entryCount); +}; + +//------------------------------------------------------------------------------ + +TER +SetSignerList::doApply () +{ + assert (mTxnAccount); + + // All operations require our ledger index. Compute that once and pass it + // to our handlers. + uint256 const index = getSignerListIndex (mTxnAccountID); + + // Perform the operation preCheck() decided on. + switch (do_) + { + case set: + return replaceSignerList (index); + + case destroy: + return destroySignerList (index); + + default: + // Fall through intentionally + break; + } + assert (false); // Should not be possible to get here. + return temMALFORMED; +} + +TER +SetSignerList::preCheck() +{ + // We need the account ID later, so do this check first. + preCheckAccount (); + + // Check the quorum. A non-zero quorum means we're creating or replacing + // the list. A zero quorum means we're destroying the list. + quorum_ = (mTxn.getFieldU32 (sfSignerQuorum)); + + bool const hasSignerEntries (mTxn.isFieldPresent (sfSignerEntries)); + if (quorum_ && hasSignerEntries) + { + SignerEntries::Decoded signers ( + SignerEntries::deserialize (mTxn, m_journal, "transaction")); + + if (signers.ter != tesSUCCESS) + return signers.ter; + + // Validate our settings. + if (TER const ter = + validateQuorumAndSignerEntries (quorum_, signers.vec)) + { + return ter; + } + + // Save deserialized and validated list for later. + signers_ = std::move (signers.vec); + do_ = set; + } + else if ((quorum_ == 0) && !hasSignerEntries) + { + do_ = destroy; + } + else + { + // Neither a set nor a destroy. Malformed. + if (m_journal.trace) m_journal.trace << + "Malformed transaction: Invalid signer set list format."; + return temMALFORMED; + } + + return preCheckSigningKey (); +} + +TER +SetSignerList::validateQuorumAndSignerEntries ( + std::uint32_t quorum, + std::vector& signers) const +{ + // Reject if there are too many or too few entries in the list. + { + std::size_t const signerCount = signers.size (); + if ((signerCount < SignerEntries::minEntries) + || (signerCount > SignerEntries::maxEntries)) + { + if (m_journal.trace) m_journal.trace << + "Too many or too few signers in signer list."; + return temMALFORMED; + } + } + + // Make sure there are no duplicate signers. + std::sort (signers.begin (), signers.end ()); + if (std::adjacent_find ( + signers.begin (), signers.end ()) != signers.end ()) + { + if (m_journal.trace) m_journal.trace << + "Duplicate signers in signer list"; + return temBAD_SIGNER; + } + + // Make sure no signers reference this account. Also make sure the + // quorum can be reached. + std::uint64_t allSignersWeight (0); + for (auto const& signer : signers) + { + allSignersWeight += signer.weight; + + if (signer.account == mTxnAccountID) + { + if (m_journal.trace) m_journal.trace << + "A signer may not self reference account."; + return temBAD_SIGNER; + } + + // Don't verify that the signer accounts exist. Non-existent accounts + // may be phantom accounts (which are permitted). + } + if ((quorum <= 0) || (allSignersWeight < quorum)) + { + if (m_journal.trace) m_journal.trace << + "Quorum is unreachable"; + return temBAD_QUORUM; + } + return tesSUCCESS; +} + +TER +SetSignerList::replaceSignerList (uint256 const& index) +{ + // This may be either a create or a replace. Preemptively destroy any + // old signer list. May reduce the reserve, so this is done before + // checking the reserve. + if (TER const ter = destroySignerList (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::uint64_t const newReserve = + mEngine->getLedger ()->getReserve (oldOwnerCount + addedOwnerCount); + + // We check the reserve against the starting balance because we want to + // allow dipping into the reserve to pay fees. This behavior is consistent + // with CreateTicket. + if (mPriorBalance < newReserve) + return tecINSUFFICIENT_RESERVE; + + // Everything's ducky. Add the ltSIGNER_LIST to the ledger. + SLE::pointer signerList ( + mEngine->view().entryCreate (ltSIGNER_LIST, index)); + writeSignersToLedger (signerList); + + // Lambda for call to dirAdd. + auto describer = [&] (SLE::ref sle, bool dummy) + { + Ledger::ownerDirDescriber (sle, dummy, mTxnAccountID); + }; + + // Add the signer list to the account's directory. + std::uint64_t hint; + TER result = mEngine->view ().dirAdd ( + hint, getOwnerDirIndex (mTxnAccountID), index, describer); + + if (m_journal.trace) m_journal.trace << + "Create signer list for account " << + mTxnAccountID << ": " << transHuman (result); + + if (result != tesSUCCESS) + return result; + + signerList->setFieldU64 (sfOwnerNode, hint); + + // If we succeeded, the new entry counts against the creator's reserve. + mEngine->view ().increaseOwnerCount (mTxnAccount, addedOwnerCount); + + return result; +} + +TER +SetSignerList::destroySignerList (uint256 const& index) +{ + // See if there's an ltSIGNER_LIST for this account. + SLE::pointer signerList = + mEngine->view ().entryCache (ltSIGNER_LIST, index); + + // If the signer list doesn't exist we've already succeeded in deleting it. + if (!signerList) + return tesSUCCESS; + + // We have to examine the current SignerList so we know how much to + // reduce the OwnerCount. + std::size_t removeFromOwnerCount = 0; + uint256 const signerListIndex = getSignerListIndex (mTxnAccountID); + SLE::pointer accountSignersList = + mEngine->view ().entryCache (ltSIGNER_LIST, signerListIndex); + if (accountSignersList) + { + STArray const& actualList = + accountSignersList->getFieldArray (sfSignerEntries); + removeFromOwnerCount = ownerCountDelta (actualList.size ()); + } + + // Remove the node from the account directory. + std::uint64_t const hint (signerList->getFieldU64 (sfOwnerNode)); + + TER const result = mEngine->view ().dirDelete (false, hint, + getOwnerDirIndex (mTxnAccountID), index, false, (hint == 0)); + + if (result == tesSUCCESS) + mEngine->view ().decreaseOwnerCount (mTxnAccount, removeFromOwnerCount); + + mEngine->view ().entryDelete (signerList); + + return result; +} + +void +SetSignerList::writeSignersToLedger (SLE::pointer ledgerEntry) +{ + // Assign the quorum. + ledgerEntry->setFieldU32 (sfSignerQuorum, quorum_); + + // Create the SignerListArray one SignerEntry at a time. + STArray toLedger (signers_.size ()); + for (auto const& entry : signers_) + { + toLedger.emplace_back(sfSignerEntry); + STObject& obj = toLedger.back(); + obj.reserve (2); + obj.setFieldAccount (sfAccount, entry.account); + obj.setFieldU16 (sfSignerWeight, entry.weight); + } + + // Assign the SignerEntries. + ledgerEntry->setFieldArray (sfSignerEntries, toLedger); +} + +std::size_t +SetSignerList::ownerCountDelta (std::size_t entryCount) +{ + // We always compute the full change in OwnerCount, taking into account: + // o The fact that we're adding/removing a SignerList and + // o Accounting for the number of entries in the list. + // We can get away with that because lists are not adjusted incrementally; + // we add or remove an entire list. + + // The wiki (https://wiki.ripple.com/Multisign#Fees_2) currently says + // (December 2014) the reserve should be + // Reserve * (N + 1) / 2 + // That's not making sense to me right now, since I'm working in + // integral OwnerCount units. If, say, N is 4 I don't know how to return + // 4.5 units as an integer. + // + // So, just to get started, I'm saying that: + // o Simply having a SignerList costs 2 OwnerCount units. + // o And each signer in the list costs 1 more OwnerCount unit. + // So, at a minimum, adding a SignerList with 2 entries costs 4 OwnerCount + // units. A SignerList with 8 entries would cost 10 OwnerCount units. + // + // It's worth noting that once this reserve policy has gotten into the + // wild it will be very difficult to change. So think hard about what + // we want for the long term. + return 2 + entryCount; +} + +TER +transact_SetSignerList ( + STTx const& txn, + TransactionEngineParams params, + TransactionEngine* engine) +{ + return SetSignerList (txn, params, engine).apply (); +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/SignerEntries.cpp b/src/ripple/app/tx/impl/SignerEntries.cpp new file mode 100644 index 000000000..0c19cb1f1 --- /dev/null +++ b/src/ripple/app/tx/impl/SignerEntries.cpp @@ -0,0 +1,67 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { + +SignerEntries::Decoded +SignerEntries::deserialize ( + STObject const& obj, beast::Journal journal, std::string const& annotation) +{ + Decoded s; + auto& accountVec (s.vec); + accountVec.reserve (maxEntries); + + if (!obj.isFieldPresent (sfSignerEntries)) + { + if (journal.trace) journal.trace << + "Malformed " << annotation << ": Need signer entry array."; + s.ter = temMALFORMED; + return s; + } + + STArray const& sEntries (obj.getFieldArray (sfSignerEntries)); + for (STObject const& sEntry : sEntries) + { + // Validate the SignerEntry. + if (sEntry.getFName () != sfSignerEntry) + { + journal.trace << + "Malformed " << annotation << ": Expected SignerEntry."; + s.ter = temMALFORMED; + return s; + } + + // Extract SignerEntry fields. + Account const account = sEntry.getFieldAccount160 (sfAccount); + std::uint16_t const weight = sEntry.getFieldU16 (sfSignerWeight); + accountVec.emplace_back (account, weight); + } + + s.ter = tesSUCCESS; + return s; +} + +} // ripple diff --git a/src/ripple/app/tx/impl/SignerEntries.h b/src/ripple/app/tx/impl/SignerEntries.h new file mode 100644 index 000000000..1ed95c518 --- /dev/null +++ b/src/ripple/app/tx/impl/SignerEntries.h @@ -0,0 +1,77 @@ +//------------------------------------------------------------------------------ +/* + 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_TX_IMPL_SIGNER_ENTRIES_H_INCLUDED +#define RIPPLE_TX_IMPL_SIGNER_ENTRIES_H_INCLUDED + +#include // STTx::maxMultiSigners +#include // Account +#include // temMALFORMED +#include // beast::Journal + +namespace ripple { + +// Forward declarations +class STObject; + +// Support for SignerEntries that is needed by a few Transactors +class SignerEntries +{ +public: + struct SignerEntry + { + Account account; + std::uint16_t weight; + + SignerEntry (Account const& inAccount, std::uint16_t inWeight) + : account (inAccount) + , weight (inWeight) + { } + + // For sorting to look for duplicate accounts + friend bool operator< (SignerEntry const& lhs, SignerEntry const& rhs) + { + return lhs.account < rhs.account; + } + + friend bool operator== (SignerEntry const& lhs, SignerEntry const& rhs) + { + return lhs.account == rhs.account; + } + }; + + struct Decoded + { + std::vector vec; + TER ter = temMALFORMED; + }; + + // Deserialize a SignerEntries array from the network or from the ledger. + static Decoded deserialize ( + STObject const& obj, + beast::Journal journal, + std::string const& annotation); + + static std::size_t const minEntries = 2; + static std::size_t const maxEntries = STTx::maxMultiSigners; +}; + +} // ripple + +#endif // RIPPLE_TX_IMPL_SIGNER_ENTRIES_H_INCLUDED diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 1122e28dc..805de75c9 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -18,8 +18,8 @@ //============================================================================== #include -#include #include +#include #include namespace ripple { @@ -33,6 +33,7 @@ TER transact_CancelOffer (STTx const& txn, TransactionEngineParams params, Trans TER transact_Change (STTx const& txn, TransactionEngineParams params, TransactionEngine* engine); TER transact_CreateTicket (STTx const& txn, TransactionEngineParams params, TransactionEngine* engine); TER transact_CancelTicket (STTx const& txn, TransactionEngineParams params, TransactionEngine* engine); +TER transact_SetSignerList (STTx const& txn, TransactionEngineParams params, TransactionEngine* engine); TER Transactor::transact ( @@ -70,6 +71,13 @@ Transactor::transact ( case ttTICKET_CANCEL: return transact_CancelTicket (txn, params, engine); +#if RIPPLE_ENABLE_MULTI_SIGN + + case ttSIGNER_LIST_SET: + return transact_SetSignerList (txn, params, engine); + +#endif // RIPPLE_ENABLE_MULTI_SIGN + default: return temUNKNOWN; } @@ -219,6 +227,15 @@ TER Transactor::checkSeq () // check stuff before you bother to lock the ledger TER Transactor::preCheck () +{ + TER result = preCheckAccount (); + if (result != tesSUCCESS) + return result; + + return preCheckSigningKey (); +} + +TER Transactor::preCheckAccount () { mTxnAccountID = mTxn.getSourceAccount ().getAccountID (); @@ -227,14 +244,19 @@ TER Transactor::preCheck () m_journal.warning << "applyTransaction: bad transaction source id"; return temBAD_SRC_ACCOUNT; } + return tesSUCCESS; +} +TER Transactor::preCheckSigningKey () +{ // Extract signing key // Transactions contain a signing key. This allows us to trivially verify a // transaction has at least been properly signed without going to disk. // Each transaction also notes a source account id. This is used to verify // that the signing key is associated with the account. // XXX This could be a lot cleaner to prevent unnecessary copying. - mSigningPubKey = RippleAddress::createAccountPublic (mTxn.getSigningPubKey ()); + mSigningPubKey = + RippleAddress::createAccountPublic (mTxn.getSigningPubKey ()); // Consistency: really signed. if (!mTxn.isKnownGood ()) diff --git a/src/ripple/app/tx/impl/Transactor.h b/src/ripple/app/tx/impl/Transactor.h index e0f2d2001..c2995099e 100644 --- a/src/ripple/app/tx/impl/Transactor.h +++ b/src/ripple/app/tx/impl/Transactor.h @@ -54,6 +54,11 @@ protected: beast::Journal m_journal; virtual TER preCheck (); + + // Non-virtual components of preCheck() + TER preCheckAccount (); + TER preCheckSigningKey (); + virtual TER checkSeq (); virtual TER payFee (); diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 4152a889c..cb962f68b 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -87,6 +87,8 @@ getRippleStateIndex (Account const& a, Account const& b, Currency const& currenc uint256 getRippleStateIndex (Account const& a, Issue const& issue); +uint256 +getSignerListIndex (Account const& account); } #endif diff --git a/src/ripple/unity/app9.cpp b/src/ripple/protocol/InnerObjectFormats.h old mode 100644 new mode 100755 similarity index 62% rename from src/ripple/unity/app9.cpp rename to src/ripple/protocol/InnerObjectFormats.h index 536d08fae..c79984128 --- a/src/ripple/unity/app9.cpp +++ b/src/ripple/protocol/InnerObjectFormats.h @@ -17,4 +17,31 @@ */ //============================================================================== -#include +#ifndef RIPPLE_PROTOCOL_INNER_OBJECT_FORMATS_H_INCLUDED +#define RIPPLE_PROTOCOL_INNER_OBJECT_FORMATS_H_INCLUDED + +#include + +namespace ripple { + +/** Manages the list of known inner object formats. +*/ +class InnerObjectFormats : public KnownFormats +{ +private: + void addCommonFields (Item& item); + +public: + /** Create the object. + This will load the object will all the known inner object formats. + */ + InnerObjectFormats (); + + static InnerObjectFormats const& getInstance (); + + SOTemplate const* findSOTemplateBySField (SField const& sField) const; +}; + +} // ripple + +#endif diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index e6fd73f72..5c064c299 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -56,6 +56,8 @@ enum LedgerEntryType ltTICKET = 'T', + ltSIGNER_LIST = 'S', + /* Deprecated. */ ltOFFER = 'o', @@ -90,6 +92,7 @@ enum LedgerNameSpace spaceAmendment = 'f', spaceFee = 'e', spaceTicket = 'T', + spaceSignerList = 'S', // No longer used or supported. Left here to reserve the space and // avoid accidental reuse of the space. diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 55bfdf3f1..41f73b471 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -295,6 +295,7 @@ extern SField const sfTransactionResult; // 16-bit integers extern SField const sfLedgerEntryType; extern SField const sfTransactionType; +extern SField const sfSignerWeight; // 32-bit integers (common) extern SField const sfFlags; @@ -331,6 +332,7 @@ extern SField const sfReserveBase; extern SField const sfReserveIncrement; extern SField const sfSetFlag; extern SField const sfClearFlag; +extern SField const sfSignerQuorum; // 64-bit integers extern SField const sfIndexNext; @@ -427,12 +429,13 @@ extern SField const sfFinalFields; extern SField const sfNewFields; extern SField const sfTemplateEntry; extern SField const sfMemo; +extern SField const sfSignerEntry; // array of objects // ARRAY/1 is reserved for end of array extern SField const sfSigningAccounts; extern SField const sfTxnSignatures; -extern SField const sfSignatures; +extern SField const sfSignerEntries; extern SField const sfTemplate; extern SField const sfNecessary; extern SField const sfSufficient; diff --git a/src/ripple/protocol/STArray.h b/src/ripple/protocol/STArray.h index 8dc3d3fc6..b854f5faf 100644 --- a/src/ripple/protocol/STArray.h +++ b/src/ripple/protocol/STArray.h @@ -40,7 +40,7 @@ private: list_type v_; public: - // Read-only iteration + // Read-only iteration class Items; static char const* getCountedObjectName () { return "STArray"; } @@ -135,6 +135,10 @@ public: { v_.clear (); } + void reserve (std::size_t n) + { + v_.reserve (n); + } void swap (STArray & a) noexcept { v_.swap (a.v_); diff --git a/src/ripple/protocol/STObject.h b/src/ripple/protocol/STObject.h index d5c37fb1e..0086228b2 100644 --- a/src/ripple/protocol/STObject.h +++ b/src/ripple/protocol/STObject.h @@ -133,7 +133,18 @@ public: return v_.empty(); } + void reserve (std::size_t n) + { + v_.reserve (n); + } + bool setType (const SOTemplate & type); + + enum ResultOfSetTypeFromSField : unsigned char + {typeSetFail, typeIsSet, noTemplate}; + + ResultOfSetTypeFromSField setTypeFromSField (SField const&); + bool isValidForType (); bool isFieldAllowed (SField const&); bool isFree () const diff --git a/src/ripple/protocol/STTx.h b/src/ripple/protocol/STTx.h index ca858df48..6b3df5ce8 100644 --- a/src/ripple/protocol/STTx.h +++ b/src/ripple/protocol/STTx.h @@ -44,6 +44,8 @@ public: typedef std::shared_ptr pointer; typedef const std::shared_ptr& ref; + static std::size_t const maxMultiSigners = 8; + public: STTx () = delete; STTx& operator= (STTx const& other) = delete; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 849ab5ebd..e858a79f0 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -81,6 +81,8 @@ enum TER // aka TransactionEngineResult temREDUNDANT, temRIPPLE_EMPTY, temDISABLED, + temBAD_SIGNER, + temBAD_QUORUM, // An intermediate result used internally, should never be returned. temUNCERTAIN, diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index a8f5eda47..12ae39b96 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -46,6 +46,7 @@ enum TxType no_longer_used = 9, ttTICKET_CREATE = 10, ttTICKET_CANCEL = 11, + ttSIGNER_LIST_SET = 12, ttTRUST_SET = 20, diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 54a43ddaa..e97429fb3 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -219,4 +219,15 @@ getRippleStateIndex (Account const& a, Issue const& issue) return getRippleStateIndex (a, issue.account, issue.currency); } +uint256 +getSignerListIndex (Account const& account) +{ + Serializer s (22); + + s.add16 (spaceSignerList); // 2 + s.add160 (account); // 20 + + return s.getSHA512Half (); } + +} // namespace ripple diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp new file mode 100755 index 000000000..c3b2a36ff --- /dev/null +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -0,0 +1,55 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { + +InnerObjectFormats::InnerObjectFormats () +{ + add (sfSignerEntry.getJsonName ().c_str (), sfSignerEntry.getCode ()) + << SOElement (sfAccount, SOE_REQUIRED) + << SOElement (sfSignerWeight, SOE_REQUIRED) + ; +} + +void InnerObjectFormats::addCommonFields (Item& item) +{ +} + +InnerObjectFormats const& +InnerObjectFormats::getInstance () +{ + static InnerObjectFormats instance; + return instance; +} + +SOTemplate const* +InnerObjectFormats::findSOTemplateBySField (SField const& sField) const +{ + SOTemplate const* ret = nullptr; + auto itemPtr = findByType (sField.getCode ()); + if (itemPtr) + ret = &(itemPtr->elements); + + return ret; +} + +} // ripple diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 954bc29ce..c01d093cc 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -105,6 +105,14 @@ LedgerFormats::LedgerFormats () << SOElement (sfTarget, SOE_OPTIONAL) << SOElement (sfExpiration, SOE_OPTIONAL) ; + + // All three fields are SOE_REQUIRED because there is always a + // SignerEntries. If there are no SignerEntries the node is deleted. + add ("SignerList", ltSIGNER_LIST) + << SOElement (sfOwnerNode, SOE_REQUIRED) + << SOElement (sfSignerQuorum, SOE_REQUIRED) + << SOElement (sfSignerEntries, SOE_REQUIRED) + ; } void LedgerFormats::addCommonFields (Item& item) diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 19ae76b83..2a2a8f07a 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -83,6 +83,7 @@ SField const sfTransactionResult = make::one(&sfTransactionResult, STI_UINT8, 3, // 16-bit integers SField const sfLedgerEntryType = make::one(&sfLedgerEntryType, STI_UINT16, 1, "LedgerEntryType", SField::sMD_Never); SField const sfTransactionType = make::one(&sfTransactionType, STI_UINT16, 2, "TransactionType"); +SField const sfSignerWeight = make::one(&sfSignerWeight, STI_UINT16, 3, "SignerWeight"); // 32-bit integers (common) SField const sfFlags = make::one(&sfFlags, STI_UINT32, 2, "Flags"); @@ -119,6 +120,7 @@ SField const sfReserveBase = make::one(&sfReserveBase, STI_UINT3 SField const sfReserveIncrement = make::one(&sfReserveIncrement, STI_UINT32, 32, "ReserveIncrement"); SField const sfSetFlag = make::one(&sfSetFlag, STI_UINT32, 33, "SetFlag"); SField const sfClearFlag = make::one(&sfClearFlag, STI_UINT32, 34, "ClearFlag"); +SField const sfSignerQuorum = make::one(&sfSignerQuorum, STI_UINT32, 35, "SignerQuorum"); // 64-bit integers SField const sfIndexNext = make::one(&sfIndexNext, STI_UINT64, 1, "IndexNext"); @@ -215,12 +217,13 @@ SField const sfFinalFields = make::one(&sfFinalFields, STI_OBJEC SField const sfNewFields = make::one(&sfNewFields, STI_OBJECT, 8, "NewFields"); SField const sfTemplateEntry = make::one(&sfTemplateEntry, STI_OBJECT, 9, "TemplateEntry"); SField const sfMemo = make::one(&sfMemo, STI_OBJECT, 10, "Memo"); +SField const sfSignerEntry = make::one(&sfSignerEntry, STI_OBJECT, 11, "SignerEntry"); // array of objects // ARRAY/1 is reserved for end of array SField const sfSigningAccounts = make::one(&sfSigningAccounts, STI_ARRAY, 2, "SigningAccounts"); SField const sfTxnSignatures = make::one(&sfTxnSignatures, STI_ARRAY, 3, "TxnSignatures", SField::sMD_Default, SField::notSigning); -SField const sfSignatures = make::one(&sfSignatures, STI_ARRAY, 4, "Signatures"); +SField const sfSignerEntries = make::one(&sfSignerEntries, STI_ARRAY, 4, "SignerEntries"); SField const sfTemplate = make::one(&sfTemplate, STI_ARRAY, 5, "Template"); SField const sfNecessary = make::one(&sfNecessary, STI_ARRAY, 6, "Necessary"); SField const sfSufficient = make::one(&sfSufficient, STI_ARRAY, 7, "Sufficient"); diff --git a/src/ripple/protocol/impl/STArray.cpp b/src/ripple/protocol/impl/STArray.cpp index 36dcaae28..e3fd03e3d 100644 --- a/src/ripple/protocol/impl/STArray.cpp +++ b/src/ripple/protocol/impl/STArray.cpp @@ -97,6 +97,11 @@ STArray::STArray (SerialIter& sit, SField const& f) v_.emplace_back(fn); v_.back().set (sit, 1); + + if (v_.back().setTypeFromSField (fn) == STObject::typeSetFail) + { + throw std::runtime_error ("Malformed object in array"); + } } } diff --git a/src/ripple/protocol/impl/STObject.cpp b/src/ripple/protocol/impl/STObject.cpp index 51c82a774..0e1e0a2da 100644 --- a/src/ripple/protocol/impl/STObject.cpp +++ b/src/ripple/protocol/impl/STObject.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -119,7 +120,7 @@ bool STObject::setType (const SOTemplate& type) { WriteLog (lsWARNING, STObject) << "setType( " << getFName ().getName () << - ") invalid default " << e->e_field.fieldName; + " ) invalid default " << e->e_field.fieldName; valid = false; } v.emplace_back(std::move(*iter)); @@ -131,7 +132,7 @@ bool STObject::setType (const SOTemplate& type) { WriteLog (lsWARNING, STObject) << "setType( " << getFName ().getName () << - ") invalid missing " << e->e_field.fieldName; + " ) invalid missing " << e->e_field.fieldName; valid = false; } v.emplace_back(detail::nonPresentObject, e->e_field); @@ -144,7 +145,7 @@ bool STObject::setType (const SOTemplate& type) { WriteLog (lsWARNING, STObject) << "setType( " << getFName ().getName () << - ") invalid leftover " << e->getFName ().getName (); + " ) invalid leftover " << e->getFName ().getName (); valid = false; } } @@ -154,6 +155,20 @@ bool STObject::setType (const SOTemplate& type) return valid; } +STObject::ResultOfSetTypeFromSField +STObject::setTypeFromSField (SField const& sField) +{ + ResultOfSetTypeFromSField ret = noTemplate; + + SOTemplate const* elements = + InnerObjectFormats::getInstance ().findSOTemplateBySField (sField); + if (elements) + { + ret = setType (*elements) ? typeIsSet : typeSetFail; + } + return ret; +} + bool STObject::isValidForType () { auto it = v_.begin(); @@ -219,6 +234,13 @@ bool STObject::set (SerialIter& sit, int depth) // Unflatten the field v_.emplace_back(sit, fn); + + // If the object type has a known SOTemplate then set it. + STObject* const obj = dynamic_cast (&(v_.back().get())); + if (obj && (obj->setTypeFromSField (fn) == typeSetFail)) + { + throw std::runtime_error ("field deserialization error"); + } } } diff --git a/src/ripple/protocol/impl/STParsedJSON.cpp b/src/ripple/protocol/impl/STParsedJSON.cpp index 92ad92700..8224b2b55 100644 --- a/src/ripple/protocol/impl/STParsedJSON.cpp +++ b/src/ripple/protocol/impl/STParsedJSON.cpp @@ -138,6 +138,26 @@ static Json::Value singleton_expected (std::string const& object, "]' must be an object with a single key/object value."); } +static Json::Value serialization_error (SField const& sField) +{ + return RPC::make_error (rpcINVALID_PARAMS, + "Object '" + sField.getName () + "' failed to serialize."); +} + +static Json::Value template_mismatch (SField const& sField) +{ + return RPC::make_error (rpcINVALID_PARAMS, + "Object '" + sField.getName () + + "' contents did not meet requirements for that type."); +} + +static Json::Value +non_object_in_array (std::string const& item, Json::UInt index) +{ + return RPC::make_error (rpcINVALID_PARAMS, + "Item '" + item + "' at index " + std::to_string (index) + + " is not an object. Arrays may only contain objects."); +} // This function is used by parseObject to parse any JSON type that doesn't // recurse. Everything represented here is a leaf-type. @@ -766,6 +786,13 @@ static boost::optional parseObject ( } } + // Some inner object types have templates. Attempt to apply that. + if (data.setTypeFromSField (inName) == STObject::typeSetFail) + { + error = template_mismatch (inName); + return boost::none; + } + return std::move (data); } @@ -823,10 +850,17 @@ static boost::optional parseArray ( auto ret = parseObject (ss.str (), objectFields, nameField, depth + 1, error); + if (! ret) + { + std::string errMsg = error["error_message"].asString (); + error["error_message"] = "Error at '" + ss.str () + + "'. " + errMsg; + return boost::none; + } - if (! ret || - (ret->getFName().fieldType != STI_OBJECT)) + if (ret->getFName().fieldType != STI_OBJECT) { + error = non_object_in_array (ss.str(), i); return boost::none; } diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 3270322d0..d57364350 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -102,6 +102,7 @@ bool transResultInfo (TER code, std::string& token, std::string& text) { temBAD_OFFER, "temBAD_OFFER", "Malformed: Bad offer." }, { temBAD_PATH, "temBAD_PATH", "Malformed: Bad path." }, { temBAD_PATH_LOOP, "temBAD_PATH_LOOP", "Malformed: Loop in path." }, + { temBAD_QUORUM, "temBAD_QUORUM", "Malformed: Quorum is unreachable." }, { temBAD_SEND_XRP_LIMIT, "temBAD_SEND_XRP_LIMIT", "Malformed: Limit quality is not allowed for XRP to XRP." }, { temBAD_SEND_XRP_MAX, "temBAD_SEND_XRP_MAX", "Malformed: Send max is not allowed for XRP to XRP." }, { temBAD_SEND_XRP_NO_DIRECT,"temBAD_SEND_XRP_NO_DIRECT","Malformed: No Ripple direct is not allowed for XRP to XRP." }, @@ -109,6 +110,7 @@ bool transResultInfo (TER code, std::string& token, std::string& text) { temBAD_SEND_XRP_PATHS, "temBAD_SEND_XRP_PATHS", "Malformed: Paths are not allowed for XRP to XRP." }, { temBAD_SEQUENCE, "temBAD_SEQUENCE", "Malformed: Sequence is not in the past." }, { temBAD_SIGNATURE, "temBAD_SIGNATURE", "Malformed: Bad signature." }, + { temBAD_SIGNER, "temBAD_SIGNER", "Malformed: A signer may not duplicate account or other signers"}, { temBAD_SRC_ACCOUNT, "temBAD_SRC_ACCOUNT", "Malformed: Bad source account." }, { temBAD_TRANSFER_RATE, "temBAD_TRANSFER_RATE", "Malformed: Transfer rate must be >= 1.0" }, { temDST_IS_SRC, "temDST_IS_SRC", "Destination may not be source." }, diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index d962d0903..9e42f1f88 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -87,6 +87,13 @@ TxFormats::TxFormats () add ("TicketCancel", ttTICKET_CANCEL) << SOElement (sfTicketID, SOE_REQUIRED) ; + + // The SignerEntries are optional because a SignerList is deleted by + // setting the SignerQuorum to zero and omitting SignerEntries. + add ("SignerListSet", ttSIGNER_LIST_SET) + << SOElement (sfSignerQuorum, SOE_REQUIRED) + << SOElement (sfSignerEntries, SOE_OPTIONAL) + ; } void TxFormats::addCommonFields (Item& item) diff --git a/src/ripple/protocol/tests/InnerObjectFormats.test.cpp b/src/ripple/protocol/tests/InnerObjectFormats.test.cpp new file mode 100755 index 000000000..e134775d0 --- /dev/null +++ b/src/ripple/protocol/tests/InnerObjectFormats.test.cpp @@ -0,0 +1,204 @@ +//------------------------------------------------------------------------------ +/* + 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 // RPC::containsError +#include // Json::Reader +#include // STParsedJSONObject +#include + +namespace ripple { + +namespace InnerObjectFormatsUnitTestDetail +{ + +struct TestJSONTxt +{ + std::string const txt; + bool const expectFail; +}; + +static TestJSONTxt const testArray[] = +{ + +// Valid SignerEntry +{R"({ + "Account" : "rDg53Haik2475DJx8bjMDSDPj4VX7htaMd", + "SignerEntries" : + [ + { + "SignerEntry" : + { + "Account" : "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "SignerWeight" : 4 + } + }, + { + "SignerEntry" : + { + "Account" : "rPcNzota6B8YBokhYtcTNqQVCngtbnWfux", + "SignerWeight" : 3 + } + } + ], + "SignerQuorum" : 7, + "TransactionType" : "SignerListSet" +})", false +}, + +// SignerEntry missing Account +{R"({ + "Account" : "rDg53Haik2475DJx8bjMDSDPj4VX7htaMd", + "SignerEntries" : + [ + { + "SignerEntry" : + { + "Account" : "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "SignerWeight" : 4 + } + }, + { + "SignerEntry" : + { + "SignerWeight" : 3 + } + } + ], + "SignerQuorum" : 7, + "TransactionType" : "SignerListSet" +})", true +}, + +// SignerEntry missing SignerWeight +{R"({ + "Account" : "rDg53Haik2475DJx8bjMDSDPj4VX7htaMd", + "SignerEntries" : + [ + { + "SignerEntry" : + { + "Account" : "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "SignerWeight" : 4 + } + }, + { + "SignerEntry" : + { + "Account" : "rPcNzota6B8YBokhYtcTNqQVCngtbnWfux", + } + } + ], + "SignerQuorum" : 7, + "TransactionType" : "SignerListSet" +})", true +}, + +// SignerEntry with unexpected Amount +{R"({ + "Account" : "rDg53Haik2475DJx8bjMDSDPj4VX7htaMd", + "SignerEntries" : + [ + { + "SignerEntry" : + { + "Account" : "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "SignerWeight" : 4 + } + }, + { + "SignerEntry" : + { + "Amount" : "1000000", + "Account" : "rPcNzota6B8YBokhYtcTNqQVCngtbnWfux", + "SignerWeight" : 3 + } + } + ], + "SignerQuorum" : 7, + "TransactionType" : "SignerListSet" +})", true +}, + +// SignerEntry with no Account and unexpected Amount +{R"({ + "Account" : "rDg53Haik2475DJx8bjMDSDPj4VX7htaMd", + "SignerEntries" : + [ + { + "SignerEntry" : + { + "Account" : "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "SignerWeight" : 4 + } + }, + { + "SignerEntry" : + { + "Amount" : "10000000", + "SignerWeight" : 3 + } + } + ], + "SignerQuorum" : 7, + "TransactionType" : "SignerListSet" +})", true +}, + +}; + +} // namespace InnerObjectFormatsUnitTestDetail + + +class InnerObjectFormatsParsedJSON_test : public beast::unit_test::suite +{ +public: + void run() + { + using namespace InnerObjectFormatsUnitTestDetail; + + for (auto const& test : testArray) + { + Json::Value req; + Json::Reader ().parse (test.txt, req); + if (RPC::contains_error (req)) + { + throw std::runtime_error ( + "Internal InnerObjectFormatsParsedJSON error. Bad JSON."); + } + STParsedJSONObject parsed ("request", req); + bool const noObj = parsed.object == boost::none; + if ( noObj == test.expectFail ) + { + pass (); + } + else + { + std::string errStr ("Unexpected STParsedJSON result on:\n"); + errStr += test.txt; + fail (errStr); + } + } + } +}; + +BEAST_DEFINE_TESTSUITE(InnerObjectFormatsParsedJSON,ripple_app,ripple); + +} // ripple diff --git a/src/ripple/rpc/handlers/AccountInfo.cpp b/src/ripple/rpc/handlers/AccountInfo.cpp index 2d9bf9591..1c39790c1 100644 --- a/src/ripple/rpc/handlers/AccountInfo.cpp +++ b/src/ripple/rpc/handlers/AccountInfo.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include namespace ripple { @@ -60,12 +61,32 @@ Json::Value doAccountInfo (RPC::Context& context) if (!jvAccepted.empty ()) return jvAccepted; - auto asAccepted = context.netOps.getAccountState (ledger, naAccount); + AccountState::pointer asAccepted = + context.netOps.getAccountState (ledger, naAccount); if (asAccepted) { asAccepted->addJson (jvAccepted); - result[jss::account_data] = jvAccepted; + + // See if there's a SignerEntries for this account. + Account const account = naAccount.getAccountID (); + uint256 const signerListIndex = getSignerListIndex (account); + SLE::pointer signerList = ledger->getSLEi (signerListIndex); + + if (signerList) + { + // Return multi-signing information if there are multi-signers. + static const Json::StaticString multiSignersName("multisigners"); + jvAccepted[multiSignersName] = signerList->getJson (0); + Json::Value& multiSignerJson = jvAccepted[multiSignersName]; + + // Remove unwanted fields. + multiSignerJson.removeMember (sfFlags.getName ()); + multiSignerJson.removeMember (sfLedgerEntryType.getName ()); + multiSignerJson.removeMember (sfOwnerNode.getName ()); + multiSignerJson.removeMember ("index"); + } + result[jss::account_data] = jvAccepted; } else { diff --git a/src/ripple/unity/app4.cpp b/src/ripple/unity/app4.cpp index 59c46a9e3..c998bf736 100644 --- a/src/ripple/unity/app4.cpp +++ b/src/ripple/unity/app4.cpp @@ -44,5 +44,8 @@ #include #include #include +#include +#include + #include #include diff --git a/src/ripple/unity/protocol.cpp b/src/ripple/unity/protocol.cpp index ab8b95563..94c18b296 100644 --- a/src/ripple/unity/protocol.cpp +++ b/src/ripple/unity/protocol.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,7 @@ #include +#include #include #include #include