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.
This commit is contained in:
Scott Schurr
2015-02-06 10:22:36 -08:00
committed by Vinnie Falco
parent 92799187ed
commit 64ebd64d2b
36 changed files with 1290 additions and 135 deletions

View File

@@ -1976,12 +1976,26 @@
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\tx\impl\SetSignerList.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\tx\impl\SetTrust.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\tx\impl\SignerEntries.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\SignerEntries.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\Taker.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -2794,6 +2808,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\protocol\impl\InnerObjectFormats.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\protocol\impl\LedgerFormats.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -2894,6 +2912,8 @@
</ClCompile>
<ClInclude Include="..\..\src\ripple\protocol\Indexes.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\InnerObjectFormats.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\Issue.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\JsonFields.h">
@@ -2960,6 +2980,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\protocol\tests\InnerObjectFormats.test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\protocol\tests\Issue.test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -3499,10 +3523,6 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\unity\app9.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\unity\basics.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">True</ExcludedFromBuild>

View File

@@ -2529,9 +2529,18 @@
<ClCompile Include="..\..\src\ripple\app\tx\impl\SetRegularKey.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\tx\impl\SetSignerList.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\tx\impl\SetTrust.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\tx\impl\SignerEntries.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\SignerEntries.h">
<Filter>ripple\app\tx\impl</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\Taker.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
@@ -3327,6 +3336,9 @@
<ClCompile Include="..\..\src\ripple\protocol\impl\Indexes.cpp">
<Filter>ripple\protocol\impl</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\protocol\impl\InnerObjectFormats.cpp">
<Filter>ripple\protocol\impl</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\protocol\impl\LedgerFormats.cpp">
<Filter>ripple\protocol\impl</Filter>
</ClCompile>
@@ -3405,6 +3417,9 @@
<ClInclude Include="..\..\src\ripple\protocol\Indexes.h">
<Filter>ripple\protocol</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\InnerObjectFormats.h">
<Filter>ripple\protocol</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\Issue.h">
<Filter>ripple\protocol</Filter>
</ClInclude>
@@ -3501,6 +3516,9 @@
<ClCompile Include="..\..\src\ripple\protocol\tests\BuildInfo.test.cpp">
<Filter>ripple\protocol\tests</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\protocol\tests\InnerObjectFormats.test.cpp">
<Filter>ripple\protocol\tests</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\protocol\tests\Issue.test.cpp">
<Filter>ripple\protocol\tests</Filter>
</ClCompile>
@@ -4068,9 +4086,6 @@
<ClCompile Include="..\..\src\ripple\unity\app8.cpp">
<Filter>ripple\unity</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\unity\app9.cpp">
<Filter>ripple\unity</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\unity\basics.cpp">
<Filter>ripple\unity</Filter>
</ClCompile>

View File

@@ -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',

View File

@@ -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

View File

@@ -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<std::uint32_t>::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<std::uint32_t>::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)

View File

@@ -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.

View File

@@ -137,7 +137,9 @@ void printHelp (const po::options_description& desc)
" ripple_path_find <json> [<ledger>]\n"
" version\n"
" server_info\n"
" sign\n"
" stop\n"
" submit\n"
" tx <id>\n"
" unl_add <domain>|<public> [<comment>]\n"
" unl_delete <domain>|<public_key>\n"

View File

@@ -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 ###

217
src/ripple/app/tx/README.md Normal file
View File

@@ -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 ###

View File

@@ -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 <BeastConfig.h>
#include <ripple/app/tx/impl/Transactor.h>
#include <ripple/app/tx/impl/SignerEntries.h>
#include <ripple/protocol/STObject.h>
#include <ripple/protocol/STArray.h>
#include <ripple/protocol/STTx.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/basics/Log.h>
#include <cstdint>
#include <algorithm>
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<SignerEntries::SignerEntry> 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<SignerEntries::SignerEntry>& 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<SignerEntries::SignerEntry>& 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

View File

@@ -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 <BeastConfig.h>
#include <ripple/app/tx/impl/SignerEntries.h>
#include <ripple/protocol/STObject.h>
#include <ripple/protocol/STArray.h>
#include <ripple/protocol/STAccount.h>
#include <cstdint>
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

View File

@@ -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 <ripple/protocol/STTx.h> // STTx::maxMultiSigners
#include <ripple/protocol/UintTypes.h> // Account
#include <ripple/protocol/TER.h> // temMALFORMED
#include <beast/utility/Journal.h> // 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<SignerEntry> 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

View File

@@ -18,8 +18,8 @@
//==============================================================================
#include <BeastConfig.h>
#include <ripple/core/Config.h>
#include <ripple/app/tx/impl/Transactor.h>
#include <ripple/core/Config.h>
#include <ripple/protocol/Indexes.h>
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 ())

View File

@@ -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 ();

View File

@@ -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

View File

@@ -17,4 +17,31 @@
*/
//==============================================================================
#include <BeastConfig.h>
#ifndef RIPPLE_PROTOCOL_INNER_OBJECT_FORMATS_H_INCLUDED
#define RIPPLE_PROTOCOL_INNER_OBJECT_FORMATS_H_INCLUDED
#include <ripple/protocol/KnownFormats.h>
namespace ripple {
/** Manages the list of known inner object formats.
*/
class InnerObjectFormats : public KnownFormats <int>
{
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

View File

@@ -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.

View File

@@ -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;

View File

@@ -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_);

View File

@@ -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

View File

@@ -44,6 +44,8 @@ public:
typedef std::shared_ptr<STTx> pointer;
typedef const std::shared_ptr<STTx>& ref;
static std::size_t const maxMultiSigners = 8;
public:
STTx () = delete;
STTx& operator= (STTx const& other) = delete;

View File

@@ -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,

View File

@@ -46,6 +46,7 @@ enum TxType
no_longer_used = 9,
ttTICKET_CREATE = 10,
ttTICKET_CANCEL = 11,
ttSIGNER_LIST_SET = 12,
ttTRUST_SET = 20,

View File

@@ -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

View File

@@ -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 <BeastConfig.h>
#include <ripple/protocol/InnerObjectFormats.h>
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

View File

@@ -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)

View File

@@ -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");

View File

@@ -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");
}
}
}

View File

@@ -21,6 +21,7 @@
#include <ripple/basics/Log.h>
#include <ripple/json/json_reader.h>
#include <ripple/json/to_string.h>
#include <ripple/protocol/InnerObjectFormats.h>
#include <ripple/protocol/STBase.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/STArray.h>
@@ -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 <STObject*> (&(v_.back().get()));
if (obj && (obj->setTypeFromSField (fn) == typeSetFail))
{
throw std::runtime_error ("field deserialization error");
}
}
}

View File

@@ -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 <STObject> 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 <detail::STVar> 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;
}

View File

@@ -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." },

View File

@@ -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)

View File

@@ -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 <BeastConfig.h>
#include <ripple/protocol/InnerObjectFormats.h>
#include <ripple/protocol/ErrorCodes.h> // RPC::containsError
#include <ripple/json/json_reader.h> // Json::Reader
#include <ripple/protocol/STParsedJSON.h> // STParsedJSONObject
#include <beast/unit_test/suite.h>
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

View File

@@ -18,6 +18,7 @@
//==============================================================================
#include <BeastConfig.h>
#include <ripple/protocol/Indexes.h>
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
{

View File

@@ -44,5 +44,8 @@
#include <ripple/app/tx/impl/CreateOffer.cpp>
#include <ripple/app/tx/impl/CreateTicket.cpp>
#include <ripple/app/tx/impl/CancelTicket.cpp>
#include <ripple/app/tx/impl/SetSignerList.cpp>
#include <ripple/app/tx/impl/SignerEntries.cpp>
#include <ripple/app/tx/tests/OfferStream.test.cpp>
#include <ripple/app/tx/tests/Taker.test.cpp>

View File

@@ -45,6 +45,7 @@
#include <ripple/protocol/impl/STLedgerEntry.cpp>
#include <ripple/protocol/impl/STObject.cpp>
#include <ripple/protocol/impl/STParsedJSON.cpp>
#include <ripple/protocol/impl/InnerObjectFormats.cpp>
#include <ripple/protocol/impl/STPathSet.cpp>
#include <ripple/protocol/impl/STTx.cpp>
#include <ripple/protocol/impl/STValidation.cpp>
@@ -53,6 +54,7 @@
#include <ripple/protocol/tests/BuildInfo.test.cpp>
#include <ripple/protocol/tests/InnerObjectFormats.test.cpp>
#include <ripple/protocol/tests/Issue.test.cpp>
#include <ripple/protocol/tests/Quality.test.cpp>
#include <ripple/protocol/tests/RippleAddress.test.cpp>