mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-18 17:45:48 +00:00
2-level transaction multi-signatures (RIPD-182):
This commit provides support for 2-level multi-signing of transactions. The ability is usually compiled out, since other aspects of multi-signing are not yet complete. Here are the missing parts: o Full support for Tickets in transactions. o Variable fees based on the number of signers, o Multiple SignerLists with access control flags on accounts, o Enable / disable operations based on access control flags, o Enable / disable all of multi-signing based on an amendment, o Integration tests, and o Documentation.
This commit is contained in:
committed by
Vinnie Falco
parent
cf1638e6de
commit
d6ef66646f
@@ -2046,6 +2046,20 @@
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\src\ripple\app\tx\LocalTxs.h">
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\tx\tests\common_transactor.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\tests\common_transactor.h">
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\tx\tests\MultiSign.test.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\tests\OfferStream.test.cpp">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||
@@ -3217,6 +3231,9 @@
|
||||
<ClCompile Include="..\..\src\ripple\rpc\handlers\Submit.cpp">
|
||||
<ExcludedFromBuild>True</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\ripple\rpc\handlers\SubmitMultiSigned.cpp">
|
||||
<ExcludedFromBuild>True</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\ripple\rpc\handlers\Subscribe.cpp">
|
||||
<ExcludedFromBuild>True</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
|
||||
@@ -2574,6 +2574,15 @@
|
||||
<ClInclude Include="..\..\src\ripple\app\tx\LocalTxs.h">
|
||||
<Filter>ripple\app\tx</Filter>
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\tx\tests\common_transactor.cpp">
|
||||
<Filter>ripple\app\tx\tests</Filter>
|
||||
</ClCompile>
|
||||
<ClInclude Include="..\..\src\ripple\app\tx\tests\common_transactor.h">
|
||||
<Filter>ripple\app\tx\tests</Filter>
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\tx\tests\MultiSign.test.cpp">
|
||||
<Filter>ripple\app\tx\tests</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\ripple\app\tx\tests\OfferStream.test.cpp">
|
||||
<Filter>ripple\app\tx\tests</Filter>
|
||||
</ClCompile>
|
||||
@@ -3747,6 +3756,9 @@
|
||||
<ClCompile Include="..\..\src\ripple\rpc\handlers\Submit.cpp">
|
||||
<Filter>ripple\rpc\handlers</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\ripple\rpc\handlers\SubmitMultiSigned.cpp">
|
||||
<Filter>ripple\rpc\handlers</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\ripple\rpc\handlers\Subscribe.cpp">
|
||||
<Filter>ripple\rpc\handlers</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@@ -1028,7 +1028,7 @@ void LedgerEntrySet::incrementOwnerCount (Account const& owner)
|
||||
}
|
||||
|
||||
void
|
||||
LedgerEntrySet::increaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch)
|
||||
LedgerEntrySet::increaseOwnerCount (SLE::ref sleAccount, std::uint32_t howMuch)
|
||||
{
|
||||
assert (sleAccount);
|
||||
|
||||
@@ -1059,7 +1059,7 @@ void LedgerEntrySet::decrementOwnerCount (Account const& owner)
|
||||
}
|
||||
|
||||
void
|
||||
LedgerEntrySet::decreaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch)
|
||||
LedgerEntrySet::decreaseOwnerCount (SLE::ref sleAccount, std::uint32_t howMuch)
|
||||
{
|
||||
assert (sleAccount);
|
||||
|
||||
|
||||
@@ -185,13 +185,13 @@ public:
|
||||
/** @{ */
|
||||
void incrementOwnerCount (SLE::ref sleAccount);
|
||||
void incrementOwnerCount (Account const& owner);
|
||||
void increaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch);
|
||||
void increaseOwnerCount (SLE::ref sleAccount, std::uint32_t howMuch);
|
||||
/** @} */
|
||||
|
||||
/** @{ */
|
||||
void decrementOwnerCount (SLE::ref sleAccount);
|
||||
void decrementOwnerCount (Account const& owner);
|
||||
void decreaseOwnerCount (SLE::ref sleAccount, std::size_t howMuch);
|
||||
void decreaseOwnerCount (SLE::ref sleAccount, std::uint32_t howMuch);
|
||||
/** @} */
|
||||
|
||||
// Offer functions.
|
||||
|
||||
@@ -143,6 +143,9 @@ void printHelp (const po::options_description& desc)
|
||||
#endif // RIPPLE_ENABLE_MULTI_SIGN
|
||||
" stop\n"
|
||||
" submit\n"
|
||||
#if RIPPLE_ENABLE_MULTI_SIGN
|
||||
" submit_multisigned\n"
|
||||
#endif // RIPPLE_ENABLE_MULTI_SIGN
|
||||
" tx <id>\n"
|
||||
" unl_add <domain>|<public> [<comment>]\n"
|
||||
" unl_delete <domain>|<public_key>\n"
|
||||
|
||||
@@ -29,8 +29,6 @@ classes the ability to replace portions of the default implementation.
|
||||
|
||||
## SetSignerList ##
|
||||
|
||||
**Associated JIRA task is [RIPD-182](https://ripplelabs.atlassian.net/browse/RIPD-182)**
|
||||
|
||||
In order to enhance the flexibility of Ripple and provide support for enhanced
|
||||
security of accounts, native support for "multi-signature" or "multi-sign"
|
||||
accounts is required.
|
||||
@@ -158,8 +156,6 @@ The data for a transaction that removes any signer list has this form:
|
||||
|
||||
## Tickets ##
|
||||
|
||||
**Associated JIRA task is [RIPD-368](https://ripplelabs.atlassian.net/browse/RIPD-368)**
|
||||
|
||||
Currently transactions on the Ripple network require the use of sequence
|
||||
numbers and sequence numbers must monotonically increase. Since the sequence
|
||||
number is part of the transaction, it is "covered" by the signature that
|
||||
|
||||
@@ -54,7 +54,7 @@ public:
|
||||
return temUNKNOWN;
|
||||
}
|
||||
|
||||
TER checkSig () override
|
||||
TER checkSign () override
|
||||
{
|
||||
if (mTxn.getFieldAccount160 (sfAccount).isNonZero ())
|
||||
{
|
||||
|
||||
@@ -193,7 +193,7 @@ public:
|
||||
{
|
||||
if (!mSigMaster)
|
||||
{
|
||||
m_journal.trace << "Can't use regular key to disable master key.";
|
||||
m_journal.trace << "Must use master key to disable master key.";
|
||||
return tecNEED_MASTER_KEY;
|
||||
}
|
||||
|
||||
|
||||
@@ -221,8 +221,8 @@ SetSignerList::replaceSignerList (uint256 const& index)
|
||||
return ter;
|
||||
|
||||
// Compute new reserve. Verify the account has funds to meet the reserve.
|
||||
std::size_t const oldOwnerCount = mTxnAccount->getFieldU32 (sfOwnerCount);
|
||||
std::size_t const addedOwnerCount = ownerCountDelta (signers_.size ());
|
||||
std::uint32_t const oldOwnerCount = mTxnAccount->getFieldU32 (sfOwnerCount);
|
||||
std::uint32_t const addedOwnerCount = ownerCountDelta (signers_.size ());
|
||||
|
||||
std::uint64_t const newReserve =
|
||||
mEngine->getLedger ()->getReserve (oldOwnerCount + addedOwnerCount);
|
||||
@@ -277,7 +277,7 @@ SetSignerList::destroySignerList (uint256 const& index)
|
||||
|
||||
// We have to examine the current SignerList so we know how much to
|
||||
// reduce the OwnerCount.
|
||||
std::size_t removeFromOwnerCount = 0;
|
||||
std::uint32_t removeFromOwnerCount = 0;
|
||||
uint256 const signerListIndex = getSignerListIndex (mTxnAccountID);
|
||||
SLE::pointer accountSignersList =
|
||||
mEngine->view ().entryCache (ltSIGNER_LIST, signerListIndex);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <BeastConfig.h>
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/app/tx/impl/SignerEntries.h>
|
||||
#include <ripple/core/Config.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
|
||||
@@ -157,35 +158,6 @@ TER Transactor::payFee ()
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER Transactor::checkSig ()
|
||||
{
|
||||
// Consistency: Check signature and verify the transaction's signing public
|
||||
// key is the key authorized for signing.
|
||||
|
||||
auto const signing_account = mSigningPubKey.getAccountID ();
|
||||
|
||||
if (signing_account == mTxnAccountID)
|
||||
{
|
||||
if (mTxnAccount->isFlag(lsfDisableMaster))
|
||||
return tefMASTER_DISABLED;
|
||||
|
||||
mSigMaster = true;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (!mHasAuthKey)
|
||||
{
|
||||
m_journal.trace << "Invalid: Not authorized to use account.";
|
||||
return temBAD_AUTH_MASTER;
|
||||
}
|
||||
|
||||
if (signing_account == mTxnAccount->getFieldAccount160 (sfRegularKey))
|
||||
return tesSUCCESS;
|
||||
|
||||
m_journal.trace << "Delay: Not authorized to use account.";
|
||||
return tefBAD_AUTH;
|
||||
}
|
||||
|
||||
TER Transactor::checkSeq ()
|
||||
{
|
||||
std::uint32_t const t_seq = mTxn.getSequence ();
|
||||
@@ -315,7 +287,7 @@ TER Transactor::apply ()
|
||||
|
||||
if (terResult != tesSUCCESS) return (terResult);
|
||||
|
||||
terResult = checkSig ();
|
||||
terResult = checkSign ();
|
||||
|
||||
if (terResult != tesSUCCESS) return (terResult);
|
||||
|
||||
@@ -325,4 +297,364 @@ TER Transactor::apply ()
|
||||
return doApply ();
|
||||
}
|
||||
|
||||
TER Transactor::checkSign ()
|
||||
{
|
||||
#if RIPPLE_ENABLE_MULTI_SIGN
|
||||
// If the mSigningPubKey is empty, then we must be multi-signing.
|
||||
TER const signingTER = mSigningPubKey.getAccountPublic ().empty () ?
|
||||
checkMultiSign () : checkSingleSign ();
|
||||
#else
|
||||
TER const signingTER = checkSingleSign ();
|
||||
#endif // RIPPLE_ENABLE_MULTI_SIGN
|
||||
|
||||
return signingTER;
|
||||
}
|
||||
|
||||
TER Transactor::checkSingleSign ()
|
||||
{
|
||||
// Consistency: Check signature
|
||||
// Verify the transaction's signing public key is authorized for signing.
|
||||
if (mSigningPubKey.getAccountID () == mTxnAccountID)
|
||||
{
|
||||
// Authorized to continue.
|
||||
mSigMaster = true;
|
||||
if (mTxnAccount->isFlag(lsfDisableMaster))
|
||||
return tefMASTER_DISABLED;
|
||||
}
|
||||
else if (mHasAuthKey &&
|
||||
(mSigningPubKey.getAccountID () ==
|
||||
mTxnAccount->getFieldAccount160 (sfRegularKey)))
|
||||
{
|
||||
// Authorized to continue.
|
||||
}
|
||||
else if (mHasAuthKey)
|
||||
{
|
||||
m_journal.trace <<
|
||||
"applyTransaction: Delay: Not authorized to use account.";
|
||||
return tefBAD_AUTH;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_journal.trace <<
|
||||
"applyTransaction: Invalid: Not authorized to use account.";
|
||||
return tefBAD_AUTH_MASTER;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
namespace TransactorDetail
|
||||
{
|
||||
|
||||
struct GetSignerListResult
|
||||
{
|
||||
TER ter = tefFAILURE;
|
||||
std::uint32_t quorum = std::numeric_limits <std::uint32_t>::max ();
|
||||
std::vector<SignerEntries::SignerEntry> signerEntries;
|
||||
};
|
||||
|
||||
// We need the SignerList for every SigningFor while multi-signing.
|
||||
GetSignerListResult
|
||||
getSignerList (
|
||||
Account signingForAcctID, TransactionEngine* engine, beast::Journal journal)
|
||||
{
|
||||
GetSignerListResult ret;
|
||||
|
||||
uint256 const index = getSignerListIndex (signingForAcctID);
|
||||
SLE::pointer accountSignersList =
|
||||
engine->view ().entryCache (ltSIGNER_LIST, index);
|
||||
|
||||
// If the signer list doesn't exist the account is not multi-signing.
|
||||
if (!accountSignersList)
|
||||
{
|
||||
journal.trace <<
|
||||
"applyTransaction: Invalid: Not a multi-signing account.";
|
||||
ret.ter = tefNOT_MULTI_SIGNING;
|
||||
return ret;
|
||||
}
|
||||
ret.quorum = accountSignersList->getFieldU32 (sfSignerQuorum);
|
||||
|
||||
SignerEntries::Decoded signersOnAccountDecode =
|
||||
SignerEntries::deserialize (*accountSignersList, journal, "ledger");
|
||||
ret.ter = signersOnAccountDecode.ter;
|
||||
|
||||
if (signersOnAccountDecode.ter == tesSUCCESS)
|
||||
{
|
||||
ret.signerEntries = std::move (signersOnAccountDecode.vec);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct CheckSigningAccountsResult
|
||||
{
|
||||
TER ter = tefFAILURE;
|
||||
std::uint32_t weightSum = 0;
|
||||
};
|
||||
|
||||
// Verify that every SigningAccount is a valid SignerEntry. Sum signer weights.
|
||||
CheckSigningAccountsResult
|
||||
checkSigningAccounts (
|
||||
std::vector<SignerEntries::SignerEntry> signerEntries,
|
||||
STArray const& signingAccounts,
|
||||
TransactionEngine* engine,
|
||||
beast::Journal journal)
|
||||
{
|
||||
CheckSigningAccountsResult ret;
|
||||
|
||||
// Both the signerEntries and signingAccounts are sorted by account. So
|
||||
// matching signerEntries to signingAccounts should be a simple
|
||||
// linear walk. *All* signers must be valid or the transaction fails.
|
||||
std::uint32_t weightSum = 0;
|
||||
auto signerEntriesItr = signerEntries.begin ();
|
||||
for (auto const& signingAccount : signingAccounts)
|
||||
{
|
||||
Account const signingAcctID =
|
||||
signingAccount.getFieldAccount (sfAccount).getAccountID ();
|
||||
|
||||
// Attempt to match the SignerEntry with a SigningAccount;
|
||||
while (signerEntriesItr->account < signingAcctID)
|
||||
{
|
||||
if (++signerEntriesItr == signerEntries.end ())
|
||||
{
|
||||
journal.trace <<
|
||||
"applyTransaction: Invalid SigningAccount.Account.";
|
||||
ret.ter = tefBAD_SIGNATURE;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
if (signerEntriesItr->account != signingAcctID)
|
||||
{
|
||||
// The SigningAccount is not in the SignerEntries.
|
||||
journal.trace <<
|
||||
"applyTransaction: Invalid SigningAccount.Account.";
|
||||
ret.ter = tefBAD_SIGNATURE;
|
||||
return ret;
|
||||
}
|
||||
if (signerEntriesItr->weight <= 0)
|
||||
{
|
||||
// The SigningAccount has a weight of zero and may not sign.
|
||||
journal.trace <<
|
||||
"applyTransaction: SigningAccount.Account needs weight > 0.";
|
||||
ret.ter = tefBAD_SIGNATURE;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// We found the SigningAccount in the list of valid signers. Now we
|
||||
// need to compute the accountID that is associated with the signer's
|
||||
// public key.
|
||||
Account const signingAcctIDFromPubKey =
|
||||
RippleAddress::createAccountPublic (
|
||||
signingAccount.getFieldVL (sfSigningPubKey)).getAccountID ();
|
||||
|
||||
// Verify that the signingAcctID and the signingAcctIDFromPubKey
|
||||
// belong together. Here is are the rules:
|
||||
//
|
||||
// 1. "Phantom account": an account that is not in the ledger
|
||||
// A. If signingAcctID == signingAcctIDFromPubKey and the
|
||||
// signingAcctID is not in the ledger then we have a phantom
|
||||
// account.
|
||||
// B. Phantom accounts are always allowed as multi-signers.
|
||||
//
|
||||
// 2. "Master Key"
|
||||
// A. signingAcctID == signingAcctIDFromPubKey, and signingAcctID
|
||||
// is in the ledger.
|
||||
// B. If the signingAcctID in the ledger does not have the
|
||||
// asfDisableMaster flag set, then the signature is allowed.
|
||||
//
|
||||
// 3. "Regular Key"
|
||||
// A. signingAcctID != signingAcctIDFromPubKey, and signingAcctID
|
||||
// is in the ledger.
|
||||
// B. If signingAcctIDFromPubKey == signingAcctID.RegularKey (from
|
||||
// ledger) then the signature is allowed.
|
||||
//
|
||||
// No other signatures are allowed. (January 2015)
|
||||
|
||||
// In any of these cases we need to know whether the account is in
|
||||
// the ledger. Determine that now.
|
||||
uint256 const signerAccountIndex = getAccountRootIndex (signingAcctID);
|
||||
|
||||
SLE::pointer signersAccountRoot =
|
||||
engine->view ().entryCache (ltACCOUNT_ROOT, signerAccountIndex);
|
||||
|
||||
if (signingAcctIDFromPubKey == signingAcctID)
|
||||
{
|
||||
// Either Phantom or Master. Phantom's automatically pass.
|
||||
if (signersAccountRoot)
|
||||
{
|
||||
// Master Key. Account may not have asfDisableMaster set.
|
||||
std::uint32_t const signerAccountFlags =
|
||||
signersAccountRoot->getFieldU32 (sfFlags);
|
||||
|
||||
if (signerAccountFlags & lsfDisableMaster)
|
||||
{
|
||||
journal.trace <<
|
||||
"applyTransaction: MultiSignature lsfDisableMaster.";
|
||||
ret.ter = tefMASTER_DISABLED;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// May be a Regular Key. Let's find out.
|
||||
// Public key must hash to the account's regular key.
|
||||
if (!signersAccountRoot)
|
||||
{
|
||||
journal.trace <<
|
||||
"applyTransaction: Non-phantom signer lacks account root.";
|
||||
ret.ter = tefBAD_SIGNATURE;
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (!signersAccountRoot->isFieldPresent (sfRegularKey))
|
||||
{
|
||||
journal.trace <<
|
||||
"applyTransaction: Account lacks RegularKey.";
|
||||
ret.ter = tefBAD_SIGNATURE;
|
||||
return ret;
|
||||
}
|
||||
if (signingAcctIDFromPubKey !=
|
||||
signersAccountRoot->getFieldAccount160 (sfRegularKey))
|
||||
{
|
||||
journal.trace <<
|
||||
"applyTransaction: Account doesn't match RegularKey.";
|
||||
ret.ter = tefBAD_SIGNATURE;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
// The signer is legitimate. Add their weight toward the quorum.
|
||||
weightSum += signerEntriesItr->weight;
|
||||
}
|
||||
ret.weightSum = weightSum;
|
||||
ret.ter = tesSUCCESS;
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace TransactorDetail
|
||||
|
||||
TER Transactor::checkMultiSign ()
|
||||
{
|
||||
// Get mTxnAccountID's SignerList and Quorum.
|
||||
using namespace TransactorDetail;
|
||||
GetSignerListResult const outer =
|
||||
getSignerList (mTxnAccountID, mEngine, m_journal);
|
||||
|
||||
if (outer.ter != tesSUCCESS)
|
||||
return outer.ter;
|
||||
|
||||
// Get the actual array of transaction signers.
|
||||
STArray const& multiSigners (mTxn.getFieldArray (sfMultiSigners));
|
||||
|
||||
// Walk the accountSigners performing a variety of checks and see if
|
||||
// the quorum is met.
|
||||
|
||||
// Both the multiSigners and accountSigners are sorted by account. So
|
||||
// matching multi-signers to account signers should be a simple
|
||||
// linear walk. *All* signers must be valid or the transaction fails.
|
||||
std::uint32_t weightSum = 0;
|
||||
auto signerEntriesItr = outer.signerEntries.begin ();
|
||||
for (auto const& signingFor : multiSigners)
|
||||
{
|
||||
Account const signingForID =
|
||||
signingFor.getFieldAccount (sfAccount).getAccountID ();
|
||||
|
||||
STArray const& signingAccounts =
|
||||
signingFor.getFieldArray (sfSigningAccounts);
|
||||
|
||||
// There are two possibilities:
|
||||
// o The signers are direct multi-signers for this account.
|
||||
// o The signers are signing for a multi-signer on this account.
|
||||
// Handle those two cases separately.
|
||||
if (signingForID == mTxnAccountID)
|
||||
{
|
||||
// The signers are direct multi-signers for this account. Results
|
||||
// from these signers directly effect the quorum.
|
||||
CheckSigningAccountsResult const outerSigningAccountsResult =
|
||||
checkSigningAccounts (
|
||||
outer.signerEntries, signingAccounts, mEngine, m_journal);
|
||||
|
||||
if (outerSigningAccountsResult.ter != tesSUCCESS)
|
||||
return outerSigningAccountsResult.ter;
|
||||
|
||||
weightSum += outerSigningAccountsResult.weightSum;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The signers are signing for a multi-signer on this account.
|
||||
// Attempt to match the signingForID with a SignerEntry
|
||||
while (signerEntriesItr->account < signingForID)
|
||||
{
|
||||
if (++signerEntriesItr == outer.signerEntries.end ())
|
||||
{
|
||||
m_journal.trace <<
|
||||
"applyTransaction: Invalid SigningFor.Account.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
}
|
||||
if (signerEntriesItr->account != signingForID)
|
||||
{
|
||||
// The signingForID is not in the SignerEntries.
|
||||
m_journal.trace <<
|
||||
"applyTransaction: Invalid SigningFor.Account.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
if (signerEntriesItr->weight <= 0)
|
||||
{
|
||||
// The SigningFor entry needs a weight greater than zero.
|
||||
m_journal.trace <<
|
||||
"applyTransaction: SigningFor.Account needs weight > 0.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
// See if the signingForID has a SignerList.
|
||||
GetSignerListResult const inner =
|
||||
getSignerList (signingForID, mEngine, m_journal);
|
||||
|
||||
if (inner.ter != tesSUCCESS)
|
||||
return inner.ter;
|
||||
|
||||
// Results from these signers indirectly effect the quorum.
|
||||
CheckSigningAccountsResult const innerSigningAccountsResult =
|
||||
checkSigningAccounts (
|
||||
inner.signerEntries, signingAccounts, mEngine, m_journal);
|
||||
|
||||
if (innerSigningAccountsResult.ter != tesSUCCESS)
|
||||
return innerSigningAccountsResult.ter;
|
||||
|
||||
// There's a policy question here. If the SigningAccounts are
|
||||
// all valid but fail to reach this signingFor's Quorum do we:
|
||||
//
|
||||
// 1. Say the signature is valid but contributes 0 toward the
|
||||
// quorum?
|
||||
//
|
||||
// 2. Say that any SigningFor that doesn't meet quorum is an
|
||||
// invalid signature and fails?
|
||||
//
|
||||
// The choice is not obvious to me. I'm picking 2 for now, since
|
||||
// it's more restrictive. We can switch to policy 1 later without
|
||||
// causing transactions that would have worked before to fail.
|
||||
// -- January 2015
|
||||
if (innerSigningAccountsResult.weightSum < inner.quorum)
|
||||
{
|
||||
m_journal.trace <<
|
||||
"applyTransaction: Level-2 SigningFor did not make quorum.";
|
||||
return tefBAD_QUORUM;
|
||||
}
|
||||
// This SigningFor met quorum. Add its weight.
|
||||
weightSum += signerEntriesItr->weight;
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot perform transaction if quorum is not met.
|
||||
if (weightSum < outer.quorum)
|
||||
{
|
||||
m_journal.trace <<
|
||||
"applyTransaction: MultiSignature failed to meet quorum.";
|
||||
return tefBAD_QUORUM;
|
||||
}
|
||||
|
||||
// Met the quorum. Continue.
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ protected:
|
||||
// Returns the fee, not scaled for load (Should be in fee units. FIXME)
|
||||
virtual std::uint64_t calculateBaseFee ();
|
||||
|
||||
virtual TER checkSig ();
|
||||
virtual TER checkSign ();
|
||||
virtual TER doApply () = 0;
|
||||
|
||||
Transactor (
|
||||
@@ -80,6 +80,10 @@ protected:
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
TER checkSingleSign ();
|
||||
TER checkMultiSign ();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
1618
src/ripple/app/tx/tests/MultiSign.test.cpp
Normal file
1618
src/ripple/app/tx/tests/MultiSign.test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
430
src/ripple/app/tx/tests/common_transactor.cpp
Normal file
430
src/ripple/app/tx/tests/common_transactor.cpp
Normal file
@@ -0,0 +1,430 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 Ripple Labs Inc.
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/tx/tests/common_transactor.h>
|
||||
#include <ripple/app/consensus/LedgerConsensus.h>
|
||||
#include <ripple/app/ledger/LedgerTiming.h>
|
||||
#include <ripple/app/tests/common_ledger.h>
|
||||
#include <ripple/basics/seconds_clock.h>
|
||||
#include <ripple/protocol/TxFormats.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
UserAccount::UserAccount (KeyType kType, std::string const& passphrase)
|
||||
{
|
||||
RippleAddress const seed = RippleAddress::createSeedGeneric (passphrase);
|
||||
master_ = generateKeysFromSeed (kType, seed);
|
||||
acctID_ = master_.publicKey.getAccountID ();
|
||||
}
|
||||
|
||||
void UserAccount::setRegKey (
|
||||
TestLedger& ledger, KeyType kType, std::string const& passphrase)
|
||||
{
|
||||
// Get information for the new regular key.
|
||||
RippleAddress const seed = RippleAddress::createSeedGeneric(passphrase);
|
||||
KeyPair regular = generateKeysFromSeed (kType, seed);
|
||||
|
||||
// Tell the ledger what we're up to.
|
||||
STTx tx = getSetRegularKeyTx (*this, regular.publicKey.getAccountID());
|
||||
singleSign (tx, *this);
|
||||
ledger.applyGoodTransaction (tx);
|
||||
|
||||
// Remember what changed.
|
||||
regular_ = regular;
|
||||
}
|
||||
|
||||
void UserAccount::clrRegKey (TestLedger& ledger)
|
||||
{
|
||||
// Tell the ledger what we're up to.
|
||||
STTx tx = getClearRegularKeyTx (*this);
|
||||
singleSign (tx, *this);
|
||||
ledger.applyGoodTransaction (tx);
|
||||
|
||||
// Remember what changed.
|
||||
RippleAddress temp;
|
||||
regular_.secretKey = temp;
|
||||
regular_.publicKey = temp;
|
||||
}
|
||||
|
||||
void UserAccount::disableMaster (TestLedger& ledger, bool doDisable)
|
||||
{
|
||||
STTx tx = getAccountSetTx (*this);
|
||||
SField const& field = doDisable ? sfSetFlag : sfClearFlag;
|
||||
tx.setFieldU32 (field, asfDisableMaster);
|
||||
singleSign (tx, *this);
|
||||
ledger.applyGoodTransaction (tx);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
TestLedger::TestLedger (
|
||||
std::uint64_t startAmountDrops,
|
||||
UserAccount const& master,
|
||||
beast::unit_test::suite& suite)
|
||||
: lastClosedLedger_()
|
||||
, openLedger_()
|
||||
, suite_(suite)
|
||||
{
|
||||
// To leverage createGenesisLedger from the Ledger tests, we must match
|
||||
// its interface.
|
||||
TestAccount const masterAcct {master.publicKey(), master.secretKey(), 0};
|
||||
std::tie (lastClosedLedger_, openLedger_) =
|
||||
createGenesisLedger(startAmountDrops, masterAcct);
|
||||
}
|
||||
|
||||
std::pair<TER, bool> TestLedger::applyTransaction (STTx const& tx, bool check)
|
||||
{
|
||||
// Apply the transaction to the open ledger.
|
||||
TransactionEngine engine(openLedger_);
|
||||
auto r = engine.applyTransaction (
|
||||
tx, tapOPEN_LEDGER | (check ? tapNONE : tapNO_CHECK_SIGN));
|
||||
|
||||
// Close the open ledger to see if the transaction was real committed.
|
||||
//
|
||||
// In part we close the open ledger so we don't have to think about the
|
||||
// time sequencing of transactions. Every transaction applied by a
|
||||
// call to this method gets applied individually. So this transaction
|
||||
// is guaranteed to be applied before the next one.
|
||||
close_and_advance(openLedger_, lastClosedLedger_);
|
||||
suite_.expect (lastClosedLedger_->assertSane() == true);
|
||||
|
||||
// Check for the transaction in the closed ledger.
|
||||
bool const foundTx =
|
||||
lastClosedLedger_->hasTransaction(tx.getTransactionID());
|
||||
suite_.expect (r.second == foundTx);
|
||||
|
||||
return {r.first, r.second && foundTx};
|
||||
}
|
||||
|
||||
void TestLedger::applyGoodTransaction (STTx const& tx, bool check)
|
||||
{
|
||||
auto ret = applyTransaction (tx, check);
|
||||
suite_.expect (ret.first == tesSUCCESS);
|
||||
suite_.expect (ret.second == true);
|
||||
}
|
||||
|
||||
void TestLedger::applyBadTransaction (STTx const& tx, TER err, bool check)
|
||||
{
|
||||
auto ret = applyTransaction (tx, check);
|
||||
suite_.expect (ret.first == err);
|
||||
suite_.expect (ret.second == false);
|
||||
}
|
||||
|
||||
void TestLedger::applyTecTransaction (STTx const& tx, TER err, bool check)
|
||||
{
|
||||
auto ret = applyTransaction (tx, check);
|
||||
suite_.expect (ret.first == err);
|
||||
suite_.expect (ret.second == true);
|
||||
}
|
||||
|
||||
AccountState::pointer
|
||||
TestLedger::getAccountState (UserAccount const& acct) const
|
||||
{
|
||||
return lastClosedLedger_->getAccountState (acct.acctPublicKey ());
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
MultiSig::MultiSig(UserAccount const& signingFor,
|
||||
UserAccount const& signer, STTx const& tx)
|
||||
: signingFor_(&signingFor)
|
||||
, signer_(&signer)
|
||||
, multiSig_()
|
||||
{
|
||||
Serializer s = tx.getMultiSigningData (
|
||||
signingFor.acctPublicKey(), signer.acctPublicKey());
|
||||
multiSig_ = signer.secretKey().accountPrivateSign (s.getData());
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void SignerList::injectInto (STTx& tx) const
|
||||
{
|
||||
// Create the SignerListArray one STObject at a time.
|
||||
STArray list (list_.size ());
|
||||
for (auto const& entry : list_)
|
||||
{
|
||||
list.emplace_back(sfSignerEntry);
|
||||
STObject& obj = list.back();
|
||||
obj.reserve (2);
|
||||
obj.setFieldAccount (sfAccount, entry.acct->getID ());
|
||||
obj.setFieldU16 (sfSignerWeight, entry.weight);
|
||||
obj.setTypeFromSField (sfSignerEntry);
|
||||
}
|
||||
// Insert the SignerEntries.
|
||||
tx.setFieldArray (sfSignerEntries, list);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Single-sign the passed transaction using acct.
|
||||
void singleSign (STTx& tx, UserAccount& acct)
|
||||
{
|
||||
tx.setFieldVL (sfSigningPubKey, acct.publicKey().getAccountPublic ());
|
||||
tx.sign (acct.secretKey ());
|
||||
}
|
||||
|
||||
// Multi-sign the passed transaction using multiSigs.
|
||||
void multiSign (STTx& tx, std::vector<MultiSig>& multiSigs)
|
||||
{
|
||||
// multiSigs must be sorted or the signature will fail.
|
||||
std::sort(multiSigs.begin(), multiSigs.end());
|
||||
insertMultiSigs(tx, multiSigs);
|
||||
}
|
||||
|
||||
// Insert the multiSigs into tx without sorting. Allows testing error cases.
|
||||
void insertMultiSigs (STTx& tx, std::vector<MultiSig> const& multiSigs)
|
||||
{
|
||||
// Know when to change out SigningFor containers.
|
||||
Account prevSigningForID;
|
||||
|
||||
// Create the MultiSigners array one STObject at a time.
|
||||
STArray multiSigners;
|
||||
boost::optional <STObject> signingFor;
|
||||
for (auto const& entry : multiSigs)
|
||||
{
|
||||
if (entry.signingForAccount() != prevSigningForID)
|
||||
{
|
||||
if (signingFor != boost::none)
|
||||
multiSigners.push_back (std::move(signingFor.get()));
|
||||
|
||||
// Construct the next SigningFor object and fill it in.
|
||||
prevSigningForID = entry.signingForAccount();
|
||||
signingFor.emplace (sfSigningFor);
|
||||
signingFor->reserve (2);
|
||||
signingFor->setFieldAccount (sfAccount, entry.signingForAccount());
|
||||
signingFor->setFieldArray (sfSigningAccounts, STArray());
|
||||
}
|
||||
assert(signingFor);
|
||||
|
||||
// Construct this SigningAccount object and fill it in.
|
||||
STArray& signingAccounts = signingFor->peekFieldArray(
|
||||
sfSigningAccounts);
|
||||
signingAccounts.emplace_back (sfSigningAccount);
|
||||
|
||||
STObject& signingAccount (signingAccounts.back());
|
||||
signingAccount.reserve (3);
|
||||
signingAccount.setFieldAccount (sfAccount, entry.signingAccount());
|
||||
signingAccount.setFieldVL (sfMultiSignature, entry.multiSignature());
|
||||
signingAccount.setFieldVL (sfSigningPubKey, entry.signingPubKey());
|
||||
}
|
||||
// Remember to put in the final SigningFor object.
|
||||
if (signingFor)
|
||||
multiSigners.push_back (std::move(signingFor.get()));
|
||||
|
||||
// Inject the multiSigners into tx.
|
||||
tx.setFieldArray (sfMultiSigners, multiSigners);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Return a transaction with an SOTemplate, sfTransactionType, sfAccount,
|
||||
// sfFee, sfFlags, and sfSequence.
|
||||
STTx getSeqTx (UserAccount& acct, TxType type)
|
||||
{
|
||||
STTx tx (type); // Sets SOTemplate and sfTransactionType.
|
||||
tx.setFieldAccount (sfAccount, acct.getID());
|
||||
tx.setFieldAmount (sfFee, STAmount (10));
|
||||
tx.setFieldU32 (sfFlags, tfUniversal);
|
||||
tx.setFieldU32 (sfSequence, acct.consumeSeq());
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return an unsigned AccountSet transaction.
|
||||
STTx getAccountSetTx (UserAccount& acct)
|
||||
{
|
||||
STTx tx = getSeqTx (acct, ttACCOUNT_SET);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return an unsigned OfferCreate transaction.
|
||||
STTx getOfferCreateTx (UserAccount& acct,
|
||||
STAmount const& takerGets, STAmount const& takerPays)
|
||||
{
|
||||
STTx tx = getSeqTx (acct, ttOFFER_CREATE);
|
||||
tx.setFieldAmount (sfTakerGets, takerGets);
|
||||
tx.setFieldAmount (sfTakerPays, takerPays);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return an unsigned OfferCancel transaction.
|
||||
STTx getOfferCancelTx (UserAccount& acct, std::uint32_t offerSeq)
|
||||
{
|
||||
STTx tx = getSeqTx (acct, ttOFFER_CANCEL);
|
||||
tx.setFieldU32 (sfOfferSequence, offerSeq);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return an unsigned transaction good for making a payment.
|
||||
STTx getPaymentTx (
|
||||
UserAccount& from, UserAccount const& to, std::uint64_t amountDrops)
|
||||
{
|
||||
STTx tx = getSeqTx (from, ttPAYMENT);
|
||||
tx.setFieldAccount (sfDestination, to.getID());
|
||||
tx.setFieldAmount (sfAmount, amountDrops);
|
||||
return tx;
|
||||
}
|
||||
|
||||
STTx getPaymentTx (
|
||||
UserAccount& from, UserAccount const& to, STAmount const& amount)
|
||||
{
|
||||
STTx tx = getSeqTx (from, ttPAYMENT);
|
||||
tx.setFieldAccount (sfDestination, to.getID());
|
||||
tx.setFieldAmount (sfAmount, amount);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return a transaction that sets a regular key
|
||||
STTx getSetRegularKeyTx (UserAccount& acct, Account const& regKey)
|
||||
{
|
||||
STTx tx = getSeqTx (acct, ttREGULAR_KEY_SET);
|
||||
tx.setFieldAccount (sfRegularKey, regKey);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return a transaction that clears a regular key
|
||||
STTx getClearRegularKeyTx (UserAccount& acct)
|
||||
{
|
||||
STTx tx = getSeqTx (acct, ttREGULAR_KEY_SET);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return a SignerListSet transaction.
|
||||
STTx getSignerListSetTx (
|
||||
UserAccount& acct, SignerList const& signers, std::uint32_t quorum)
|
||||
{
|
||||
STTx tx = getSeqTx (acct, ttSIGNER_LIST_SET);
|
||||
tx.setFieldU32 (sfSignerQuorum, quorum);
|
||||
if (! signers.empty ())
|
||||
{
|
||||
signers.injectInto (tx);
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return a transaction that creates an un-targeted ticket.
|
||||
STTx getCreateTicketTx (UserAccount& acct)
|
||||
{
|
||||
STTx tx = getSeqTx (acct, ttTICKET_CREATE);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return a transaction that creates a targeted ticket.
|
||||
STTx getCreateTicketTx (UserAccount& acct, UserAccount const& target)
|
||||
{
|
||||
STTx tx = getSeqTx (acct, ttTICKET_CREATE);
|
||||
tx.setFieldAccount (sfTarget, target.getID());
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return a transaction that cancels a ticket.
|
||||
STTx getCancelTicketTx (UserAccount& acct, uint256 const& ticketID)
|
||||
{
|
||||
STTx tx = getSeqTx (acct, ttTICKET_CANCEL);
|
||||
tx.setFieldH256 (sfTicketID, ticketID);
|
||||
return tx;
|
||||
}
|
||||
|
||||
// Return a trust set transaction.
|
||||
STTx getTrustSetTx (UserAccount& from, Issue const& issuer, int limit)
|
||||
{
|
||||
STTx tx = getSeqTx (from, ttTRUST_SET);
|
||||
STAmount const stLimit (issuer, limit);
|
||||
tx.setFieldAmount (sfLimitAmount, stLimit);
|
||||
return tx;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void payInDrops (TestLedger& ledger,
|
||||
UserAccount& from, UserAccount const& to, std::uint64_t amountDrops)
|
||||
{
|
||||
STTx tx = getPaymentTx (from, to, amountDrops);
|
||||
singleSign (tx, from);
|
||||
ledger.applyGoodTransaction (tx);
|
||||
}
|
||||
|
||||
std::uint64_t getNativeBalance(TestLedger& ledger, UserAccount& acct)
|
||||
{
|
||||
return getNValue(ledger.getAccountState(acct)->getBalance());
|
||||
}
|
||||
|
||||
std::uint32_t getOwnerCount(TestLedger& ledger, UserAccount& acct)
|
||||
{
|
||||
return ledger.getAccountState(acct)->getSLE()->getFieldU32 (sfOwnerCount);
|
||||
}
|
||||
|
||||
std::vector<RippleState::pointer> getRippleStates (
|
||||
TestLedger& ledger, UserAccount const& acct, UserAccount const& peer)
|
||||
{
|
||||
std::vector <RippleState::pointer> states;
|
||||
|
||||
ledger.openLedger()->visitAccountItems (
|
||||
acct.getID(),
|
||||
[&states, &acct, &peer](SLE::ref sleCur)
|
||||
{
|
||||
// See whether this SLE is a lt_RIPPLE_STATE
|
||||
if (!sleCur || sleCur->getType () != ltRIPPLE_STATE)
|
||||
return;
|
||||
|
||||
// It's a lt_RIPPLE_STATE. See if it's one we want to return.
|
||||
RippleState::pointer const state (
|
||||
RippleState::makeItem (acct.getID(), sleCur));
|
||||
|
||||
if ((state) && (state->getAccountIDPeer() == peer.getID()))
|
||||
states.emplace_back (std::move (state));
|
||||
});
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
std::vector <SLE::pointer>
|
||||
getOffersOnAccount (TestLedger& ledger, UserAccount const& acct)
|
||||
{
|
||||
std::vector <SLE::pointer> offers;
|
||||
|
||||
ledger.openLedger()->visitAccountItems (
|
||||
acct.getID(),
|
||||
[&offers, &acct](SLE::ref sleCur)
|
||||
{
|
||||
// If sleCur is an ltOFFER save it.
|
||||
if (sleCur && sleCur->getType () == ltOFFER)
|
||||
offers.emplace_back (sleCur);
|
||||
});
|
||||
return offers;
|
||||
}
|
||||
|
||||
std::vector <SLE::pointer>
|
||||
getTicketsOnAccount (TestLedger& ledger, UserAccount const& acct)
|
||||
{
|
||||
std::vector <SLE::pointer> offers;
|
||||
|
||||
ledger.openLedger()->visitAccountItems (
|
||||
acct.getID(),
|
||||
[&offers, &acct](SLE::ref sleCur)
|
||||
{
|
||||
// If sleCur is an ltTICKET save it.
|
||||
if (sleCur && sleCur->getType () == ltTICKET)
|
||||
offers.emplace_back (sleCur);
|
||||
});
|
||||
return offers;
|
||||
}
|
||||
|
||||
} // test
|
||||
} // ripple
|
||||
360
src/ripple/app/tx/tests/common_transactor.h
Normal file
360
src/ripple/app/tx/tests/common_transactor.h
Normal file
@@ -0,0 +1,360 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 Ripple Labs Inc.
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_TRANSACTORS_TESTS_COMMON_TRANSACTOR_H_INCLUDED
|
||||
#define RIPPLE_APP_TRANSACTORS_TESTS_COMMON_TRANSACTOR_H_INCLUDED
|
||||
|
||||
#include <ripple/app/ledger/Ledger.h>
|
||||
#include <ripple/app/paths/RippleState.h>
|
||||
#include <ripple/protocol/RippleAddress.h>
|
||||
#include <ripple/protocol/STAmount.h>
|
||||
#include <beast/unit_test/suite.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
// Forward declare TestLedger so UserAccount can take it as a parameter.
|
||||
class TestLedger;
|
||||
|
||||
// This class makes it easier to write unit tests that involve user accounts.
|
||||
class UserAccount
|
||||
{
|
||||
private:
|
||||
KeyPair master_;
|
||||
Account acctID_;
|
||||
KeyPair regular_;
|
||||
bool useRegKey_ = false;
|
||||
std::uint32_t sequence_ = 0;
|
||||
|
||||
public:
|
||||
UserAccount () = delete;
|
||||
UserAccount (UserAccount const& rhs) = default;
|
||||
UserAccount& operator= (UserAccount const& rhs) = default;
|
||||
|
||||
UserAccount (KeyType kType, std::string const& passphrase);
|
||||
|
||||
Account const& getID () const
|
||||
{
|
||||
return acctID_;
|
||||
}
|
||||
|
||||
// Sets the regular key on the account, but does not disable master.
|
||||
void setRegKey (
|
||||
TestLedger& ledger, KeyType kType, std::string const& passphrase);
|
||||
|
||||
// Removes the regular key.
|
||||
void clrRegKey (TestLedger& ledger);
|
||||
|
||||
// Either disables or enables the master key.
|
||||
void disableMaster (TestLedger& ledger, bool doDisable);
|
||||
|
||||
// Select to use either the regular (true) or master (false) key.
|
||||
void useRegKey (bool useReg)
|
||||
{
|
||||
useRegKey_ = useReg;
|
||||
}
|
||||
|
||||
std::uint32_t consumeSeq ()
|
||||
{
|
||||
return ++sequence_;
|
||||
}
|
||||
|
||||
// If a transaction fails we have to back up the sequence number, since.
|
||||
// the last sequence wasn't consumed.
|
||||
void decrSeq ()
|
||||
{
|
||||
--sequence_;
|
||||
}
|
||||
|
||||
RippleAddress const& acctPublicKey () const
|
||||
{
|
||||
return master_.publicKey;
|
||||
}
|
||||
|
||||
RippleAddress const& publicKey () const
|
||||
{
|
||||
return useRegKey_ ? regular_.publicKey : master_.publicKey;
|
||||
}
|
||||
|
||||
RippleAddress const& secretKey () const
|
||||
{
|
||||
return useRegKey_ ? regular_.secretKey : master_.secretKey;
|
||||
}
|
||||
};
|
||||
|
||||
// This class collects a bunch of the ledger shenanigans tests have to do.
|
||||
class TestLedger
|
||||
{
|
||||
private:
|
||||
Ledger::pointer lastClosedLedger_;
|
||||
Ledger::pointer openLedger_;
|
||||
beast::unit_test::suite& suite_;
|
||||
|
||||
public:
|
||||
TestLedger () = delete;
|
||||
TestLedger (const TestLedger& lhs) = delete;
|
||||
TestLedger (
|
||||
std::uint64_t startAmountDrops,
|
||||
UserAccount const& master,
|
||||
beast::unit_test::suite& suite);
|
||||
|
||||
// Return.first : transaction's TEC
|
||||
// Return.second : transaction successfully applied and in ledger.
|
||||
std::pair<TER, bool> applyTransaction (STTx const& tx, bool check = true);
|
||||
|
||||
// Apply a transaction that we expect to succeed.
|
||||
void applyGoodTransaction (STTx const& tx, bool check = true);
|
||||
|
||||
// Apply a transaction that we expect to fail. Pass the expected error code.
|
||||
void applyBadTransaction (STTx const& tx, TER err, bool check = true);
|
||||
|
||||
// apply a transaction that we expect to fail but charges a fee. Pass the
|
||||
// expected error code.
|
||||
void applyTecTransaction (STTx const& tx, TER err, bool check = true);
|
||||
|
||||
// Return the AccountState for a UserAccount
|
||||
AccountState::pointer getAccountState (UserAccount const& acct) const;
|
||||
|
||||
// Return the current open ledger.
|
||||
Ledger::pointer openLedger ()
|
||||
{
|
||||
return openLedger_;
|
||||
}
|
||||
};
|
||||
|
||||
// This class makes it easier to construct unit tests with SignerLists.
|
||||
//
|
||||
// Typically construct this class with an initializer list. Like this:
|
||||
//
|
||||
// UserAccount alice;
|
||||
// UserAccount becky;
|
||||
// SignerList s {{alice, 5}, {becky, 2}};
|
||||
//
|
||||
// where '5' and '2' are the weights of the accounts in the SignerList.
|
||||
class SignerList
|
||||
{
|
||||
private:
|
||||
// Our initializer list takes references that we turn into pointers.
|
||||
struct InitListEntry
|
||||
{
|
||||
UserAccount& acct;
|
||||
std::uint16_t weight;
|
||||
};
|
||||
|
||||
struct SignerAndWeight
|
||||
{
|
||||
UserAccount* acct;
|
||||
std::uint16_t weight;
|
||||
SignerAndWeight(InitListEntry& entry)
|
||||
: acct (&entry.acct)
|
||||
, weight (entry.weight) { }
|
||||
};
|
||||
std::vector<SignerAndWeight> list_;
|
||||
|
||||
public:
|
||||
SignerList() = default;
|
||||
SignerList(SignerList const& rhs) = default;
|
||||
SignerList(SignerList&& rhs) // Visual Studio 2013 won't default move ctor
|
||||
: list_(std::move(rhs.list_))
|
||||
{ }
|
||||
SignerList(std::initializer_list<InitListEntry> args)
|
||||
{
|
||||
list_.reserve(args.size());
|
||||
for (auto arg : args)
|
||||
{
|
||||
list_.emplace_back(arg);
|
||||
}
|
||||
}
|
||||
|
||||
SignerList& operator=(SignerList const& rhs) = default;
|
||||
SignerList& operator=(SignerList&& rhs) // VS 2013 won't default move assign
|
||||
{
|
||||
list_ = std::move(rhs.list_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool empty() const
|
||||
{
|
||||
return list_.empty ();
|
||||
}
|
||||
|
||||
// Inject this SignerList into the passed transaction.
|
||||
void injectInto (STTx& tx) const;
|
||||
};
|
||||
|
||||
// This type makes it easier to write unit tests that use multi-signatures.
|
||||
//
|
||||
// The MultiSig type is intended to be short-lived (any user account it
|
||||
// references should out-live it). It is also intended to be easily
|
||||
// moved in a std::vector, so it uses pointers not references.
|
||||
class MultiSig
|
||||
{
|
||||
UserAccount const* signingFor_;
|
||||
UserAccount const* signer_;
|
||||
Blob multiSig_;
|
||||
|
||||
public:
|
||||
MultiSig() = delete;
|
||||
MultiSig(MultiSig const& rhs) = default;
|
||||
MultiSig(MultiSig&& rhs) // Visual Studio 2013 won't default move ctor
|
||||
: signingFor_ (std::move(rhs.signingFor_))
|
||||
, signer_ (std::move(rhs.signer_))
|
||||
, multiSig_ (std::move(rhs.multiSig_))
|
||||
{ }
|
||||
|
||||
MultiSig& operator=(MultiSig const& rhs) = delete;
|
||||
MultiSig& operator=(MultiSig&& rhs) // VS 2013 won't default move assign
|
||||
{
|
||||
signingFor_ = std::move(rhs.signingFor_);
|
||||
signer_ = std::move(rhs.signer_);
|
||||
multiSig_ = std::move(rhs.multiSig_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
MultiSig(UserAccount const& signingFor,
|
||||
UserAccount const& signer, STTx const& tx);
|
||||
|
||||
friend bool operator==(MultiSig const& lhs, MultiSig const& rhs)
|
||||
{
|
||||
return ((lhs.signingFor_->getID() == rhs.signingFor_->getID()) &&
|
||||
(lhs.signer_->getID() == rhs.signer_->getID()));
|
||||
}
|
||||
|
||||
friend bool operator<(MultiSig const& lhs, MultiSig const& rhs)
|
||||
{
|
||||
Account const& lhsSigingFor = lhs.signingFor_->getID();
|
||||
Account const& rhsSigningFor = rhs.signingFor_->getID();
|
||||
if (lhsSigingFor < rhsSigningFor)
|
||||
return true;
|
||||
|
||||
if (lhsSigingFor > rhsSigningFor)
|
||||
return false;
|
||||
|
||||
if (lhs.signer_->getID() < rhs.signer_->getID())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Account const& signingForAccount() const
|
||||
{
|
||||
return signingFor_->getID();
|
||||
}
|
||||
|
||||
Account const& signingAccount() const
|
||||
{
|
||||
return signer_->getID();
|
||||
}
|
||||
|
||||
Blob const& multiSignature() const
|
||||
{
|
||||
return multiSig_;
|
||||
}
|
||||
|
||||
Blob const& signingPubKey() const
|
||||
{
|
||||
return signer_->publicKey().getAccountPublic();
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Single-sign the passed transaction using acct.
|
||||
void singleSign (STTx& tx, UserAccount& acct);
|
||||
|
||||
// Multi-sign the passed transaction using multiSigs.
|
||||
void multiSign (STTx& tx, std::vector<MultiSig>& multiSigs);
|
||||
|
||||
// Insert the multiSigs into tx without sorting. Allows testing error cases.
|
||||
void insertMultiSigs (STTx& tx, std::vector<MultiSig> const& multiSigs);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Return a transaction with an SOTemplate, sfTransactionType, sfAccount,
|
||||
// sfFee, sfFlags, and sfSequence.
|
||||
STTx getSeqTx (UserAccount& acct, TxType type);
|
||||
|
||||
// Get an AccountSet transaction.
|
||||
STTx getAccountSetTx (UserAccount& acct);
|
||||
|
||||
// Return an unsigned OfferCreate transaction.
|
||||
STTx getOfferCreateTx (UserAccount& acct,
|
||||
STAmount const& takerGets, STAmount const& takerPays);
|
||||
|
||||
// Return an unsigned OfferCancel transaction.
|
||||
STTx getOfferCancelTx (UserAccount& acct, std::uint32_t offerSeq);
|
||||
|
||||
// Return an unsigned transaction good for making a payment in XRP.
|
||||
STTx getPaymentTx (
|
||||
UserAccount& from, UserAccount const& to, std::uint64_t amountDrops);
|
||||
|
||||
// Return an unsigned transaction good for making a payment.
|
||||
STTx getPaymentTx (
|
||||
UserAccount& from, UserAccount const& to, STAmount const& amount);
|
||||
|
||||
// Return a transaction that sets a regular key.
|
||||
STTx getSetRegularKeyTx (UserAccount& acct, Account const& regKey);
|
||||
|
||||
// Return a transaction that clears a regular key.
|
||||
STTx getClearRegularKeyTx (UserAccount& acct);
|
||||
|
||||
// Return a SignerListSet transaction. If the quorum is zero and signers
|
||||
// is empty, then any signer list is removed from the account.
|
||||
STTx getSignerListSetTx (
|
||||
UserAccount& acct, SignerList const& signers, std::uint32_t quorum);
|
||||
|
||||
// Return a transaction that creates an un-targeted ticket.
|
||||
STTx getCreateTicketTx (UserAccount& acct);
|
||||
|
||||
// Return a transaction that creates an targeted ticket.
|
||||
STTx getCreateTicketTx (UserAccount& acct, UserAccount const& target);
|
||||
|
||||
// Return a transaction that cancels a ticket.
|
||||
STTx getCancelTicketTx (UserAccount& acct, uint256 const& ticketID);
|
||||
|
||||
// Return an unsigned trust set transaction.
|
||||
STTx getTrustSetTx (UserAccount& from, Issue const& issuer, int limit);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Complete the a simple Payment transaction in drops. Expected to succeed.
|
||||
void payInDrops (TestLedger& ledger,
|
||||
UserAccount& from, UserAccount const& to, std::uint64_t amountDrops);
|
||||
|
||||
// Return the native balance on an account.
|
||||
std::uint64_t getNativeBalance(TestLedger& ledger, UserAccount& acct);
|
||||
|
||||
// Return the owner count of an account.
|
||||
std::uint32_t getOwnerCount(TestLedger& ledger, UserAccount& acct);
|
||||
|
||||
// Get all RippleStates between two accounts.
|
||||
std::vector<RippleState::pointer> getRippleStates (
|
||||
TestLedger& ledger, UserAccount const& from, UserAccount const& peer);
|
||||
|
||||
// Get all Offers on an account.
|
||||
std::vector <SLE::pointer>
|
||||
getOffersOnAccount (TestLedger& ledger, UserAccount const& acct);
|
||||
|
||||
// Get all Tickets on an account.
|
||||
std::vector <SLE::pointer>
|
||||
getTicketsOnAccount (TestLedger& ledger, UserAccount const& acct);
|
||||
|
||||
} // test
|
||||
} // ripple
|
||||
|
||||
#endif // RIPPLE_APP_TRANSACTORS_TESTS_COMMON_TRANSACTOR_H_INCLUDED
|
||||
@@ -659,6 +659,28 @@ private:
|
||||
return rpcError (rpcINVALID_PARAMS);
|
||||
}
|
||||
|
||||
// submit any multisigned transaction to the network
|
||||
//
|
||||
// submit_multisigned <json>
|
||||
Json::Value parseSubmitMultiSigned (Json::Value const& jvParams)
|
||||
{
|
||||
Json::Value jvRequest;
|
||||
Json::Reader reader;
|
||||
bool const bOffline = 2 == jvParams.size () && jvParams[1u].asString () == "offline";
|
||||
|
||||
if ((1 == jvParams.size () || bOffline)
|
||||
&& reader.parse (jvParams[0u].asString (), jvRequest))
|
||||
{
|
||||
// Multisigned.
|
||||
if (bOffline)
|
||||
jvRequest["offline"] = true;
|
||||
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
return rpcError (rpcINVALID_PARAMS);
|
||||
}
|
||||
|
||||
// tx <transaction_id>
|
||||
Json::Value parseTx (Json::Value const& jvParams)
|
||||
{
|
||||
@@ -859,6 +881,9 @@ public:
|
||||
{ "sign_for", &RPCParser::parseSignFor, 4, 4 },
|
||||
#endif // RIPPLE_ENABLE_MULTI_SIGN
|
||||
{ "submit", &RPCParser::parseSignSubmit, 1, 3 },
|
||||
#if RIPPLE_ENABLE_MULTI_SIGN
|
||||
{ "submit_multisigned", &RPCParser::parseSubmitMultiSigned, 1, 1 },
|
||||
#endif // RIPPLE_ENABLE_MULTI_SIGN
|
||||
{ "server_info", &RPCParser::parseAsIs, 0, 0 },
|
||||
{ "server_state", &RPCParser::parseAsIs, 0, 0 },
|
||||
{ "stop", &RPCParser::parseAsIs, 0, 0 },
|
||||
|
||||
@@ -157,6 +157,9 @@ public:
|
||||
std::string const& escapedMetaData) const;
|
||||
|
||||
private:
|
||||
bool checkSingleSign () const;
|
||||
bool checkMultiSign () const;
|
||||
|
||||
TxType tx_type_;
|
||||
|
||||
mutable boost::tribool sig_state_;
|
||||
|
||||
@@ -56,7 +56,6 @@ enum TER // aka TransactionEngineResult
|
||||
temMALFORMED = -299,
|
||||
|
||||
temBAD_AMOUNT,
|
||||
temBAD_AUTH_MASTER,
|
||||
temBAD_CURRENCY,
|
||||
temBAD_EXPIRATION,
|
||||
temBAD_FEE,
|
||||
@@ -113,6 +112,10 @@ enum TER // aka TransactionEngineResult
|
||||
tefWRONG_PRIOR,
|
||||
tefMASTER_DISABLED,
|
||||
tefMAX_LEDGER,
|
||||
tefBAD_SIGNATURE,
|
||||
tefBAD_QUORUM,
|
||||
tefNOT_MULTI_SIGNING,
|
||||
tefBAD_AUTH_MASTER,
|
||||
|
||||
// -99 .. -1: R Retry
|
||||
// sequence too high, no funds for txn fee, originating -account
|
||||
|
||||
@@ -28,6 +28,17 @@ InnerObjectFormats::InnerObjectFormats ()
|
||||
<< SOElement (sfAccount, SOE_REQUIRED)
|
||||
<< SOElement (sfSignerWeight, SOE_REQUIRED)
|
||||
;
|
||||
|
||||
add (sfSigningFor.getJsonName ().c_str (), sfSigningFor.getCode ())
|
||||
<< SOElement (sfAccount, SOE_REQUIRED)
|
||||
<< SOElement (sfSigningAccounts, SOE_REQUIRED)
|
||||
;
|
||||
|
||||
add (sfSigningAccount.getJsonName ().c_str (), sfSigningAccount.getCode ())
|
||||
<< SOElement (sfAccount, SOE_REQUIRED)
|
||||
<< SOElement (sfSigningPubKey, SOE_REQUIRED)
|
||||
<< SOElement (sfMultiSignature, SOE_REQUIRED)
|
||||
;
|
||||
}
|
||||
|
||||
void InnerObjectFormats::addCommonFields (Item& item)
|
||||
|
||||
@@ -18,17 +18,18 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <BeastConfig.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/HashPrefix.h>
|
||||
#include <ripple/protocol/JsonFields.h>
|
||||
#include <ripple/protocol/Protocol.h>
|
||||
#include <ripple/protocol/STAccount.h>
|
||||
#include <ripple/protocol/STArray.h>
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/basics/StringUtilities.h>
|
||||
#include <ripple/json/to_string.h>
|
||||
#include <beast/unit_test/suite.h>
|
||||
#include <beast/cxx14/memory.h> // <memory>
|
||||
#include <boost/format.hpp>
|
||||
#include <array>
|
||||
|
||||
@@ -195,15 +196,16 @@ bool STTx::checkSign () const
|
||||
{
|
||||
try
|
||||
{
|
||||
ECDSA const fullyCanonical = (getFlags() & tfFullyCanonicalSig)
|
||||
? ECDSA::strict
|
||||
: ECDSA::not_strict;
|
||||
|
||||
RippleAddress n;
|
||||
n.setAccountPublic (getFieldVL (sfSigningPubKey));
|
||||
|
||||
sig_state_ = n.accountPublicVerify (getSigningData (*this),
|
||||
getFieldVL (sfTxnSignature), fullyCanonical);
|
||||
#if RIPPLE_ENABLE_MULTI_SIGN
|
||||
// Determine whether we're single- or multi-signing by looking
|
||||
// at the SigningPubKey. It it's empty we must be multi-signing.
|
||||
// Otherwise we're single-signing.
|
||||
Blob const& signingPubKey = getFieldVL (sfSigningPubKey);
|
||||
sig_state_ = signingPubKey.empty () ?
|
||||
checkMultiSign () : checkSingleSign ();
|
||||
#else
|
||||
sig_state_ = checkSingleSign ();
|
||||
#endif // RIPPLE_ENABLE_MULTI_SIGN
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -280,6 +282,217 @@ STTx::getMetaSQL (Serializer rawTxn,
|
||||
% getSequence () % inLedger % status % rTxn % escapedMetaData);
|
||||
}
|
||||
|
||||
bool
|
||||
STTx::checkSingleSign () const
|
||||
{
|
||||
// We don't allow both a non-empty sfSigningPubKey and an sfMultiSigners.
|
||||
// That would allow the transaction to be signed two ways. So if both
|
||||
// fields are present the signature is invalid.
|
||||
if (isFieldPresent (sfMultiSigners))
|
||||
return false;
|
||||
|
||||
bool ret = false;
|
||||
try
|
||||
{
|
||||
ECDSA const fullyCanonical = (getFlags() & tfFullyCanonicalSig)
|
||||
? ECDSA::strict
|
||||
: ECDSA::not_strict;
|
||||
|
||||
RippleAddress n;
|
||||
n.setAccountPublic (getFieldVL (sfSigningPubKey));
|
||||
|
||||
ret = n.accountPublicVerify (getSigningData (*this),
|
||||
getFieldVL (sfTxnSignature), fullyCanonical);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Assume it was a signature failure.
|
||||
ret = false;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool
|
||||
STTx::checkMultiSign () const
|
||||
{
|
||||
// Make sure the MultiSigners are present. Otherwise they are not
|
||||
// attempting multi-signing and we just have a bad SigningPubKey.
|
||||
if (!isFieldPresent (sfMultiSigners))
|
||||
return false;
|
||||
|
||||
STArray const& multiSigners (getFieldArray (sfMultiSigners));
|
||||
|
||||
// There are well known bounds that the number of signers must be within.
|
||||
{
|
||||
std::size_t const multiSignerCount = multiSigners.size ();
|
||||
if ((multiSignerCount < 1) || (multiSignerCount > maxMultiSigners))
|
||||
return false;
|
||||
}
|
||||
|
||||
// We can ease the computational load inside the loop a bit by
|
||||
// pre-constructing part of the data that we hash. Fill a Serializer
|
||||
// with the stuff that stays constant from signature to signature.
|
||||
Serializer const dataStart (startMultiSigningData ());
|
||||
|
||||
// We also use the sfAccount field inside the loop. Get it once.
|
||||
RippleAddress const txnAccountID = getFieldAccount (sfAccount);
|
||||
|
||||
// Determine whether signatures must be full canonical.
|
||||
ECDSA const fullyCanonical = (getFlags() & tfFullyCanonicalSig)
|
||||
? ECDSA::strict
|
||||
: ECDSA::not_strict;
|
||||
|
||||
// We need to detect (and reject) if a multi-signer is both signing
|
||||
// directly and using a SigningFor. Here's an example:
|
||||
//
|
||||
// {
|
||||
// ...
|
||||
// "MultiSigners": [
|
||||
// {
|
||||
// "SigningFor": {
|
||||
// "Account": "<alice>",
|
||||
// "SigningAccounts": [
|
||||
// {
|
||||
// "SigningAccount": {
|
||||
// // * becky says that becky signs for alice. *
|
||||
// "Account": "<becky>",
|
||||
// ...
|
||||
// "SigningFor": {
|
||||
// "Account": "<becky>",
|
||||
// "SigningAccounts": [
|
||||
// {
|
||||
// "SigningAccount": {
|
||||
// // * cheri says that becky signs for alice. *
|
||||
// "Account": "<cheri>",
|
||||
// ...
|
||||
// "tx_json": {
|
||||
// "Account": "<alice>",
|
||||
// ...
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Why is this way of signing a problem? Alice has a signer list, and
|
||||
// Becky can show up in that list only once. By design. So if Becky
|
||||
// signs twice -- once directly and once indirectly -- we have three
|
||||
// options:
|
||||
//
|
||||
// 1. We can add Becky's weight toward Alice's quorum twice, once for
|
||||
// each signature. This seems both unexpected and counter to Alice's
|
||||
// intention.
|
||||
//
|
||||
// 2. We could allow both signatures, but only add Becky's weight
|
||||
// toward Alice's quorum once. This seems a bit better. But it allows
|
||||
// our clients to ask rippled to do more work than necessary. We
|
||||
// should also let the client know that only one of the signatures
|
||||
// was necessary.
|
||||
//
|
||||
// 3. The only way to tell the client that they have done more work
|
||||
// than necessary (and that one of the signatures will be ignored) is
|
||||
// to declare the transaction malformed. This behavior also aligns
|
||||
// well with rippled's behavior if Becky had signed directly twice:
|
||||
// the transaction would be marked as malformed.
|
||||
//
|
||||
// We use this std::set to detect this form of double-signing.
|
||||
std::set<RippleAddress> firstLevelSigners;
|
||||
|
||||
// SigningFors must be in sorted order by AccountID.
|
||||
RippleAddress lastSigningForID;
|
||||
lastSigningForID.setAccountID ("");
|
||||
|
||||
// Every signature must verify or we reject the transaction.
|
||||
for (auto const& signingFor : multiSigners)
|
||||
{
|
||||
RippleAddress const signingForID =
|
||||
signingFor.getFieldAccount (sfAccount);
|
||||
|
||||
// SigningFors must be in order by account ID. No duplicates allowed.
|
||||
if (lastSigningForID >= signingForID)
|
||||
return false;
|
||||
|
||||
// The next SigningFor must be greater than this one.
|
||||
lastSigningForID = signingForID;
|
||||
|
||||
// If signingForID is *not* txnAccountID, then look for duplicates.
|
||||
bool const directSigning = (signingForID == txnAccountID);
|
||||
if (! directSigning)
|
||||
{
|
||||
if (! firstLevelSigners.insert (signingForID).second)
|
||||
// This is a duplicate signer. Fail.
|
||||
return false;
|
||||
}
|
||||
|
||||
STArray const& signingAccounts (
|
||||
signingFor.getFieldArray (sfSigningAccounts));
|
||||
|
||||
// There are bounds that the number of signers must be within.
|
||||
{
|
||||
std::size_t const signingAccountsCount = signingAccounts.size ();
|
||||
if ((signingAccountsCount < 1) ||
|
||||
(signingAccountsCount > maxMultiSigners))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// SingingAccounts must be in sorted order by AccountID.
|
||||
RippleAddress lastSigningAcctID;
|
||||
lastSigningAcctID.setAccountID ("");
|
||||
|
||||
for (auto const& signingAcct : signingAccounts)
|
||||
{
|
||||
RippleAddress const signingAcctID =
|
||||
signingAcct.getFieldAccount (sfAccount);
|
||||
|
||||
// None of the multi-signers may sign for themselves.
|
||||
if (signingForID == signingAcctID)
|
||||
return false;
|
||||
|
||||
// Accounts must be in order by account ID. No duplicates allowed.
|
||||
if (lastSigningAcctID >= signingAcctID)
|
||||
return false;
|
||||
|
||||
// The next signature must be greater than this one.
|
||||
lastSigningAcctID = signingAcctID;
|
||||
|
||||
// If signingForID *is* txnAccountID, then look for duplicates.
|
||||
if (directSigning)
|
||||
{
|
||||
if (! firstLevelSigners.insert (signingAcctID).second)
|
||||
// This is a duplicate signer. Fail.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the signature.
|
||||
bool validSig = false;
|
||||
try
|
||||
{
|
||||
Serializer s = dataStart;
|
||||
finishMultiSigningData (signingForID, signingAcctID, s);
|
||||
|
||||
RippleAddress const pubKey =
|
||||
RippleAddress::createAccountPublic (
|
||||
signingAcct.getFieldVL (sfSigningPubKey));
|
||||
|
||||
Blob const signature =
|
||||
signingAcct.getFieldVL (sfMultiSignature);
|
||||
|
||||
validSig = pubKey.accountPublicVerify (
|
||||
s.getData(), signature, fullyCanonical);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// We assume any problem lies with the signature.
|
||||
validSig = false;
|
||||
}
|
||||
if (!validSig)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// All signatures verified.
|
||||
return true;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
static
|
||||
|
||||
@@ -73,6 +73,8 @@ bool transResultInfo (TER code, std::string& token, std::string& text)
|
||||
{ tefBAD_ADD_AUTH, "tefBAD_ADD_AUTH", "Not authorized to add account." },
|
||||
{ tefBAD_AUTH, "tefBAD_AUTH", "Transaction's public key is not authorized." },
|
||||
{ tefBAD_LEDGER, "tefBAD_LEDGER", "Ledger in unexpected state." },
|
||||
{ tefBAD_QUORUM, "tefBAD_QUORUM", "Signatures provided do not meet the quorum." },
|
||||
{ tefBAD_SIGNATURE, "tefBAD_SIGNATURE", "A signature is provided for a non-signer." },
|
||||
{ tefCREATED, "tefCREATED", "Can't add an already created account." },
|
||||
{ tefEXCEPTION, "tefEXCEPTION", "Unexpected program state." },
|
||||
{ tefFAILURE, "tefFAILURE", "Failed to apply." },
|
||||
@@ -80,8 +82,10 @@ bool transResultInfo (TER code, std::string& token, std::string& text)
|
||||
{ tefMASTER_DISABLED, "tefMASTER_DISABLED", "Master key is disabled." },
|
||||
{ tefMAX_LEDGER, "tefMAX_LEDGER", "Ledger sequence too high." },
|
||||
{ tefNO_AUTH_REQUIRED, "tefNO_AUTH_REQUIRED", "Auth is not required." },
|
||||
{ tefNOT_MULTI_SIGNING, "tefNOT_MULTI_SIGNING", "Account has no appropriate list of multi-signers." },
|
||||
{ tefPAST_SEQ, "tefPAST_SEQ", "This sequence number has already past." },
|
||||
{ tefWRONG_PRIOR, "tefWRONG_PRIOR", "This previous transaction does not match." },
|
||||
{ tefBAD_AUTH_MASTER, "tefBAD_AUTH_MASTER", "Auth for unclaimed account needs correct master key." },
|
||||
|
||||
{ telLOCAL_ERROR, "telLOCAL_ERROR", "Local failure." },
|
||||
{ telBAD_DOMAIN, "telBAD_DOMAIN", "Domain too long." },
|
||||
@@ -93,7 +97,6 @@ bool transResultInfo (TER code, std::string& token, std::string& text)
|
||||
|
||||
{ temMALFORMED, "temMALFORMED", "Malformed transaction." },
|
||||
{ temBAD_AMOUNT, "temBAD_AMOUNT", "Can only send positive amounts." },
|
||||
{ temBAD_AUTH_MASTER, "temBAD_AUTH_MASTER", "Auth for unclaimed account needs correct master key." },
|
||||
{ temBAD_CURRENCY, "temBAD_CURRENCY", "Malformed: Bad currency." },
|
||||
{ temBAD_EXPIRATION, "temBAD_EXPIRATION", "Malformed: Bad expiration." },
|
||||
{ temBAD_FEE, "temBAD_FEE", "Invalid fee, negative or not XRP." },
|
||||
|
||||
@@ -112,6 +112,7 @@ void TxFormats::addCommonFields (Item& item)
|
||||
<< SOElement(sfMemos, SOE_OPTIONAL)
|
||||
<< SOElement(sfSigningPubKey, SOE_REQUIRED)
|
||||
<< SOElement(sfTxnSignature, SOE_OPTIONAL)
|
||||
<< SOElement(sfMultiSigners, SOE_OPTIONAL) // submit_multisigned
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,134 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class InnerObjectFormatsSerializer_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
void run()
|
||||
{
|
||||
// Create a transaction
|
||||
RippleAddress txnSeed;
|
||||
txnSeed.setSeedRandom ();
|
||||
RippleAddress txnGenerator = txnSeed.createGeneratorPublic (txnSeed);
|
||||
RippleAddress txnPublicAcct = txnSeed.createAccountPublic (txnGenerator, 1);
|
||||
|
||||
STTx txn (ttACCOUNT_SET);
|
||||
txn.setSourceAccount (txnPublicAcct);
|
||||
txn.setSigningPubKey (txnPublicAcct);
|
||||
txn.setFieldVL (sfMessageKey, txnPublicAcct.getAccountPublic ());
|
||||
Blob const emptyBlob; // Make empty signature for multi-signing
|
||||
txn.setFieldVL (sfSigningPubKey, emptyBlob);
|
||||
|
||||
// Create fields for a SigningAccount
|
||||
RippleAddress saSeed;
|
||||
saSeed.setSeedGeneric ("masterpassphrase");
|
||||
RippleAddress const saGenerator = saSeed.createGeneratorPublic (saSeed);
|
||||
RippleAddress const saPublicAcct =
|
||||
saSeed.createAccountPublic (saGenerator, 1);
|
||||
Account const saID = saPublicAcct.getAccountID ();
|
||||
|
||||
// Create a field for SigningFor
|
||||
Account const signingForID = txnPublicAcct.getAccountID ();
|
||||
|
||||
RippleAddress saPrivateAcct =
|
||||
saSeed.createAccountPrivate(saGenerator, saSeed, 0);
|
||||
|
||||
// Get the stream of the transaction for use in multi-signing.
|
||||
Serializer s = txn.getMultiSigningData (saPublicAcct, saPublicAcct);
|
||||
|
||||
Blob saMultiSignature =
|
||||
saPrivateAcct.accountPrivateSign (s.getData());
|
||||
|
||||
// The InnerObjectFormats say a SigningAccount is supposed to look
|
||||
// like this:
|
||||
// SigningAccount {
|
||||
// Account: "...",
|
||||
// MultiSignature: "...",
|
||||
// PublicKey: "...""
|
||||
// }
|
||||
// Make one well formed SigningAccount and several mal-formed ones.
|
||||
// See whether the serializer lets the good one through and catches
|
||||
// the bad ones.
|
||||
|
||||
// This lambda contains the bulk of the test code.
|
||||
auto testMalformedSigningAccount =
|
||||
[this, &txn, &signingForID]
|
||||
(STObject const& signingAcct, bool expectPass)
|
||||
{
|
||||
// Create SigningAccounts array.
|
||||
STArray signingAccts (sfSigningAccounts, 1);
|
||||
signingAccts.push_back (signingAcct);
|
||||
|
||||
// Insert SigningAccounts array into SigningFor object.
|
||||
STObject signingFor (sfSigningFor);
|
||||
signingFor.setFieldAccount (sfAccount, signingForID);
|
||||
signingFor.setFieldArray (sfSigningAccounts, signingAccts);
|
||||
|
||||
// Insert SigningFor into MultiSigners.
|
||||
STArray multiSigners (sfMultiSigners, 1);
|
||||
multiSigners.push_back (signingFor);
|
||||
|
||||
// Insert MultiSigners into transaction.
|
||||
STTx tempTxn (txn);
|
||||
tempTxn.setFieldArray (sfMultiSigners, multiSigners);
|
||||
|
||||
Serializer rawTxn;
|
||||
tempTxn.add (rawTxn);
|
||||
SerialIter sit (rawTxn);
|
||||
bool serialized = false;
|
||||
try
|
||||
{
|
||||
STTx copy (sit);
|
||||
serialized = true;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
; // If it threw then serialization failed.
|
||||
}
|
||||
expect (serialized == expectPass,
|
||||
"Unexpected serialized = " + std::to_string (serialized) +
|
||||
". Object:\n" + signingAcct.getFullText () + "\n");
|
||||
};
|
||||
|
||||
{
|
||||
// Test case 1. Make a valid SigningAccount object.
|
||||
STObject soTest1 (sfSigningAccount);
|
||||
soTest1.setFieldAccount (sfAccount, saID);
|
||||
soTest1.setFieldVL (sfSigningPubKey,
|
||||
txnPublicAcct.getAccountPublic ());
|
||||
soTest1.setFieldVL (sfMultiSignature, saMultiSignature);
|
||||
testMalformedSigningAccount (soTest1, true);
|
||||
}
|
||||
{
|
||||
// Test case 2. Omit sfSigningPubKey from SigningAccount.
|
||||
STObject soTest2 (sfSigningAccount);
|
||||
soTest2.setFieldAccount (sfAccount, saID);
|
||||
soTest2.setFieldVL (sfMultiSignature, saMultiSignature);
|
||||
testMalformedSigningAccount (soTest2, false);
|
||||
}
|
||||
{
|
||||
// Test case 3. Extra sfAmount in SigningAccount.
|
||||
STObject soTest3 (sfSigningAccount);
|
||||
soTest3.setFieldAccount (sfAccount, saID);
|
||||
soTest3.setFieldVL (sfSigningPubKey,
|
||||
txnPublicAcct.getAccountPublic ());
|
||||
soTest3.setFieldVL (sfMultiSignature, saMultiSignature);
|
||||
soTest3.setFieldAmount (sfAmount, STAmount (10000));
|
||||
testMalformedSigningAccount (soTest3, false);
|
||||
}
|
||||
{
|
||||
// Test case 4. Right number of fields, but wrong ones.
|
||||
STObject soTest4 (sfSigningAccount);
|
||||
soTest4.setFieldVL (sfSigningPubKey,
|
||||
txnPublicAcct.getAccountPublic ());
|
||||
soTest4.setFieldVL (sfMultiSignature, saMultiSignature);
|
||||
soTest4.setFieldAmount (sfAmount, STAmount (10000));
|
||||
testMalformedSigningAccount (soTest4, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(STTx,ripple_app,ripple);
|
||||
BEAST_DEFINE_TESTSUITE(InnerObjectFormatsSerializer,ripple_app,ripple);
|
||||
|
||||
} // ripple
|
||||
|
||||
39
src/ripple/rpc/handlers/SubmitMultiSigned.cpp
Normal file
39
src/ripple/rpc/handlers/SubmitMultiSigned.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012-2014 Ripple Labs Inc.
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <BeastConfig.h>
|
||||
#include <ripple/basics/StringUtilities.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// {
|
||||
// SigningAccounts <array>,
|
||||
// tx_json: <object>,
|
||||
// }
|
||||
Json::Value doSubmitMultiSigned (RPC::Context& context)
|
||||
{
|
||||
context.loadType = Resource::feeHighBurdenRPC;
|
||||
|
||||
NetworkOPs::FailHard const failType = NetworkOPs::doFailHard (
|
||||
context.params.isMember ("fail_hard")
|
||||
&& context.params["fail_hard"].asBool ());
|
||||
|
||||
return RPC::transactionSubmitMultiSigned (
|
||||
context.params, failType, context.netOps, context.role);
|
||||
}
|
||||
|
||||
} // ripple
|
||||
@@ -137,6 +137,9 @@ HandlerTable HANDLERS({
|
||||
{ "sign_for", byRef (&doSignFor), Role::USER, NO_CONDITION },
|
||||
#endif // RIPPLE_ENABLE_MULTI_SIGN
|
||||
{ "submit", byRef (&doSubmit), Role::USER, NEEDS_CURRENT_LEDGER },
|
||||
#if RIPPLE_ENABLE_MULTI_SIGN
|
||||
{ "submit_multisigned", byRef (&doSubmitMultiSigned), Role::USER, NEEDS_CURRENT_LEDGER },
|
||||
#endif // RIPPLE_ENABLE_MULTI_SIGN
|
||||
{ "server_info", byRef (&doServerInfo), Role::USER, NO_CONDITION },
|
||||
{ "server_state", byRef (&doServerState), Role::USER, NO_CONDITION },
|
||||
{ "stop", byRef (&doStop), Role::ADMIN, NO_CONDITION },
|
||||
|
||||
@@ -525,7 +525,6 @@ struct transactionPreProcessResult
|
||||
transactionPreProcessResult& operator=
|
||||
(transactionPreProcessResult&&) = delete;
|
||||
|
||||
|
||||
transactionPreProcessResult (Json::Value&& json)
|
||||
: first (std::move (json))
|
||||
, second ()
|
||||
@@ -1019,5 +1018,276 @@ Json::Value transactionSignFor (
|
||||
return json;
|
||||
}
|
||||
|
||||
/** Returns a Json::objectValue. */
|
||||
Json::Value transactionSubmitMultiSigned (
|
||||
Json::Value jvRequest,
|
||||
NetworkOPs::FailHard failType,
|
||||
detail::TxnSignApiFacade& apiFacade,
|
||||
Role role)
|
||||
{
|
||||
WriteLog (lsDEBUG, RPCHandler)
|
||||
<< "transactionSubmitMultiSigned: " << jvRequest;
|
||||
|
||||
// When multi-signing, the "Sequence" and "SigningPubKey" fields must
|
||||
// be passed in by the caller.
|
||||
using namespace detail;
|
||||
{
|
||||
Json::Value err = checkMultiSignFields (jvRequest);
|
||||
if (RPC::contains_error (err))
|
||||
return std::move (err);
|
||||
}
|
||||
|
||||
Json::Value& tx_json (jvRequest ["tx_json"]);
|
||||
|
||||
auto const txJsonResult = checkTxJsonFields(tx_json, apiFacade, role, true);
|
||||
if (RPC::contains_error (txJsonResult.first))
|
||||
return std::move (txJsonResult.first);
|
||||
|
||||
RippleAddress const raSrcAddressID = std::move (txJsonResult.second);
|
||||
|
||||
apiFacade.snapshotAccountState (raSrcAddressID);
|
||||
if (!apiFacade.isValidAccount ())
|
||||
{
|
||||
// If not offline and did not find account, error.
|
||||
WriteLog (lsDEBUG, RPCHandler)
|
||||
<< "transactionSubmitMultiSigned: Failed to find source account "
|
||||
<< "in current ledger: "
|
||||
<< raSrcAddressID.humanAccountID ();
|
||||
|
||||
return rpcError (rpcSRC_ACT_NOT_FOUND);
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value err = checkFee (jvRequest, apiFacade, role, AutoFill::dont);
|
||||
if (RPC::contains_error(err))
|
||||
return std::move (err);
|
||||
|
||||
err = checkPayment (
|
||||
jvRequest,
|
||||
tx_json,
|
||||
raSrcAddressID,
|
||||
apiFacade,
|
||||
role,
|
||||
PathFind::dont);
|
||||
|
||||
if (RPC::contains_error(err))
|
||||
return std::move (err);
|
||||
}
|
||||
|
||||
// Grind through the JSON in tx_json to produce a STTx
|
||||
STTx::pointer stpTrans;
|
||||
{
|
||||
STParsedJSONObject parsedTx_json ("tx_json", tx_json);
|
||||
if (!parsedTx_json.object)
|
||||
{
|
||||
Json::Value jvResult;
|
||||
jvResult ["error"] = parsedTx_json.error ["error"];
|
||||
jvResult ["error_code"] = parsedTx_json.error ["error_code"];
|
||||
jvResult ["error_message"] = parsedTx_json.error ["error_message"];
|
||||
return jvResult;
|
||||
}
|
||||
try
|
||||
{
|
||||
stpTrans =
|
||||
std::make_shared<STTx> (std::move(parsedTx_json.object.get()));
|
||||
}
|
||||
catch (std::exception& ex)
|
||||
{
|
||||
std::string reason (ex.what ());
|
||||
return RPC::make_error (rpcINTERNAL,
|
||||
"Exception while serializing transaction: " + reason);
|
||||
}
|
||||
std::string reason;
|
||||
if (!passesLocalChecks (*stpTrans, reason))
|
||||
return RPC::make_error (rpcINVALID_PARAMS, reason);
|
||||
}
|
||||
|
||||
// Validate the fields in the serialized transaction.
|
||||
{
|
||||
// We now have the transaction text serialized and in the right format.
|
||||
// Verify the values of select fields.
|
||||
//
|
||||
// The SigningPubKey must be present but empty.
|
||||
if (!stpTrans->getFieldVL (sfSigningPubKey).empty ())
|
||||
{
|
||||
std::ostringstream err;
|
||||
err << "Invalid " << sfSigningPubKey.fieldName
|
||||
<< " field. Field must be empty when multi-signing.";
|
||||
return RPC::make_error (rpcINVALID_PARAMS, err.str ());
|
||||
}
|
||||
|
||||
// The Fee field must be greater than zero.
|
||||
if (stpTrans->getFieldAmount (sfFee) <= 0)
|
||||
{
|
||||
std::ostringstream err;
|
||||
err << "Invalid " << sfFee.fieldName
|
||||
<< " field. Value must be greater than zero.";
|
||||
return RPC::make_error (rpcINVALID_PARAMS, err.str ());
|
||||
}
|
||||
}
|
||||
|
||||
// Check MultiSigners for valid entries.
|
||||
const char* multiSignersArrayName {sfMultiSigners.getJsonName ().c_str ()};
|
||||
|
||||
STArray multiSigners;
|
||||
{
|
||||
// Verify that the MultiSigners field is present and an array.
|
||||
if (! jvRequest.isMember (multiSignersArrayName))
|
||||
return RPC::missing_field_error (multiSignersArrayName);
|
||||
|
||||
Json::Value& multiSignersValue (
|
||||
jvRequest [multiSignersArrayName]);
|
||||
|
||||
if (! multiSignersValue.isArray ())
|
||||
{
|
||||
std::ostringstream err;
|
||||
err << "Expected "
|
||||
<< multiSignersArrayName << " to be an array";
|
||||
return RPC::make_param_error (err.str ());
|
||||
}
|
||||
|
||||
// Convert the MultiSigners into SerializedTypes.
|
||||
STParsedJSONArray parsedMultiSigners (
|
||||
multiSignersArrayName, multiSignersValue);
|
||||
|
||||
if (!parsedMultiSigners.array)
|
||||
{
|
||||
Json::Value jvResult;
|
||||
jvResult ["error"] = parsedMultiSigners.error ["error"];
|
||||
jvResult ["error_code"] =
|
||||
parsedMultiSigners.error ["error_code"];
|
||||
jvResult ["error_message"] =
|
||||
parsedMultiSigners.error ["error_message"];
|
||||
return jvResult;
|
||||
}
|
||||
multiSigners = std::move (parsedMultiSigners.array.get());
|
||||
}
|
||||
|
||||
if (multiSigners.empty ())
|
||||
return RPC::make_param_error ("MultiSigners array may not be empty.");
|
||||
|
||||
for (STObject const& signingFor : multiSigners)
|
||||
{
|
||||
if (signingFor.getFName () != sfSigningFor)
|
||||
return RPC::make_param_error (
|
||||
"MultiSigners array has a non-SigningFor entry");
|
||||
|
||||
// Each SigningAccounts array must have at least one entry.
|
||||
STArray const& signingAccounts =
|
||||
signingFor.getFieldArray (sfSigningAccounts);
|
||||
|
||||
if (signingAccounts.empty ())
|
||||
return RPC::make_param_error (
|
||||
"A SigningAccounts array may not be empty");
|
||||
|
||||
// Each SigningAccounts array may only contain SigningAccount objects.
|
||||
auto const signingAccountsEnd = signingAccounts.end ();
|
||||
auto const findItr = std::find_if (
|
||||
signingAccounts.begin (), signingAccountsEnd,
|
||||
[](STObject const& obj)
|
||||
{ return obj.getFName () != sfSigningAccount; });
|
||||
|
||||
if (findItr != signingAccountsEnd)
|
||||
return RPC::make_param_error (
|
||||
"SigningAccounts may only contain SigningAccount objects.");
|
||||
}
|
||||
|
||||
// Lambdas for sorting arrays and finding duplicates.
|
||||
auto byFieldAccountID =
|
||||
[] (STObject const& a, STObject const& b) {
|
||||
return (a.getFieldAccount (sfAccount).getAccountID () <
|
||||
b.getFieldAccount (sfAccount).getAccountID ()); };
|
||||
|
||||
auto ifDuplicateAccountID =
|
||||
[] (STObject const& a, STObject const& b) {
|
||||
return (a.getFieldAccount (sfAccount).getAccountID () ==
|
||||
b.getFieldAccount (sfAccount).getAccountID ()); };
|
||||
|
||||
{
|
||||
// MultiSigners are submitted sorted in Account order. This
|
||||
// assures that the same list will always have the same hash.
|
||||
multiSigners.sort (byFieldAccountID);
|
||||
|
||||
// There may be no duplicate Accounts in MultiSigners
|
||||
auto const multiSignersEnd = multiSigners.end ();
|
||||
|
||||
auto const dupAccountItr = std::adjacent_find (
|
||||
multiSigners.begin (), multiSignersEnd, ifDuplicateAccountID);
|
||||
|
||||
if (dupAccountItr != multiSignersEnd)
|
||||
{
|
||||
std::ostringstream err;
|
||||
err << "Duplicate SigningFor:Account entries ("
|
||||
<< dupAccountItr->getFieldAccount (sfAccount).humanAccountID ()
|
||||
<< ") are not allowed.";
|
||||
return RPC::make_param_error(err.str ());
|
||||
}
|
||||
}
|
||||
|
||||
// All SigningAccounts inside the MultiSigners must also be sorted and
|
||||
// contain no duplicates.
|
||||
for (STObject& signingFor : multiSigners)
|
||||
{
|
||||
STArray& signingAccts = signingFor.peekFieldArray (sfSigningAccounts);
|
||||
signingAccts.sort (byFieldAccountID);
|
||||
|
||||
auto const signingAcctsEnd = signingAccts.end ();
|
||||
|
||||
auto const dupAccountItr = std::adjacent_find (
|
||||
signingAccts.begin (), signingAcctsEnd, ifDuplicateAccountID);
|
||||
|
||||
if (dupAccountItr != signingAcctsEnd)
|
||||
{
|
||||
std::ostringstream err;
|
||||
err << "Duplicate SigningAccounts:SigningAccount:Account entries ("
|
||||
<< dupAccountItr->getFieldAccount (sfAccount).humanAccountID ()
|
||||
<< ") are not allowed.";
|
||||
return RPC::make_param_error(err.str ());
|
||||
}
|
||||
|
||||
// An account may not sign for itself.
|
||||
RippleAddress const signingForAcct =
|
||||
signingFor.getFieldAccount (sfAccount);
|
||||
|
||||
auto const selfSigningItr = std::find_if(
|
||||
signingAccts.begin (), signingAcctsEnd,
|
||||
[&signingForAcct](STObject const& elem)
|
||||
{ return elem.getFieldAccount (sfAccount) == signingForAcct; });
|
||||
|
||||
if (selfSigningItr != signingAcctsEnd)
|
||||
{
|
||||
std::ostringstream err;
|
||||
err << "A SigningAccount may not SignFor itself ("
|
||||
<< signingForAcct.humanAccountID () << ").";
|
||||
return RPC::make_param_error(err.str ());
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the MultiSigners into the transaction.
|
||||
stpTrans->setFieldArray (sfMultiSigners, std::move(multiSigners));
|
||||
|
||||
// Make sure the SerializedTransaction makes a legitimate Transaction.
|
||||
std::pair <Json::Value, Transaction::pointer> txn =
|
||||
transactionConstructImpl (stpTrans);
|
||||
|
||||
if (!txn.second)
|
||||
return txn.first;
|
||||
|
||||
// Finally, submit the transaction.
|
||||
try
|
||||
{
|
||||
// FIXME: For performance, should use asynch interface
|
||||
apiFacade.processTransaction (
|
||||
txn.second, role == Role::ADMIN, true, failType);
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
return RPC::make_error (rpcINTERNAL,
|
||||
"Exception occurred during transaction submission.");
|
||||
}
|
||||
|
||||
return transactionFormatResultImpl (txn.second);
|
||||
}
|
||||
|
||||
} // RPC
|
||||
} // ripple
|
||||
|
||||
@@ -168,6 +168,24 @@ Json::Value transactionSignFor (
|
||||
return transactionSignFor (params, failType, apiFacade, role);
|
||||
}
|
||||
|
||||
/** Returns a Json::objectValue. */
|
||||
Json::Value transactionSubmitMultiSigned (
|
||||
Json::Value params, // Passed by value so it can be modified locally.
|
||||
NetworkOPs::FailHard failType,
|
||||
detail::TxnSignApiFacade& apiFacade,
|
||||
Role role);
|
||||
|
||||
/** Returns a Json::objectValue. */
|
||||
Json::Value transactionSubmitMultiSigned (
|
||||
Json::Value const& params,
|
||||
NetworkOPs::FailHard failType,
|
||||
NetworkOPs& netOPs,
|
||||
Role role)
|
||||
{
|
||||
detail::TxnSignApiFacade apiFacade (netOPs);
|
||||
return transactionSubmitMultiSigned (params, failType, apiFacade, role);
|
||||
}
|
||||
|
||||
} // RPC
|
||||
} // ripple
|
||||
|
||||
|
||||
@@ -963,7 +963,8 @@ public:
|
||||
{
|
||||
TestStuff {transactionSign, "sign", 0},
|
||||
TestStuff {transactionSubmit, "submit", 1},
|
||||
TestStuff {transactionSignFor, "sign_for", 2}
|
||||
TestStuff {transactionSignFor, "sign_for", 2},
|
||||
TestStuff {transactionSubmitMultiSigned, "submit_multisigned", 3}
|
||||
};
|
||||
|
||||
for (auto testFunc : testFuncs)
|
||||
|
||||
@@ -47,5 +47,7 @@
|
||||
#include <ripple/app/tx/impl/SetSignerList.cpp>
|
||||
#include <ripple/app/tx/impl/SignerEntries.cpp>
|
||||
|
||||
#include <ripple/app/tx/tests/common_transactor.cpp>
|
||||
#include <ripple/app/tx/tests/MultiSign.test.cpp>
|
||||
#include <ripple/app/tx/tests/OfferStream.test.cpp>
|
||||
#include <ripple/app/tx/tests/Taker.test.cpp>
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
#include <ripple/rpc/handlers/SignFor.cpp>
|
||||
#include <ripple/rpc/handlers/Stop.cpp>
|
||||
#include <ripple/rpc/handlers/Submit.cpp>
|
||||
#include <ripple/rpc/handlers/SubmitMultiSigned.cpp>
|
||||
#include <ripple/rpc/handlers/Subscribe.cpp>
|
||||
#include <ripple/rpc/handlers/TransactionEntry.cpp>
|
||||
#include <ripple/rpc/handlers/Tx.cpp>
|
||||
|
||||
Reference in New Issue
Block a user