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