mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-24 15:40:08 +00:00
Compare commits
11 Commits
wasmtime-e
...
multi-sig-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7245a161f8 | ||
|
|
3cd7a30754 | ||
|
|
e27eb4f12d | ||
|
|
37bfa0af49 | ||
|
|
69b82beafa | ||
|
|
198ff08ee5 | ||
|
|
d6aad6744e | ||
|
|
339f1b7f6f | ||
|
|
bb6553193e | ||
|
|
ffcfd94327 | ||
|
|
1baeb9fb33 |
@@ -195,6 +195,71 @@ STTx::getTransactionID() const
|
|||||||
return tid_;
|
return tid_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Multi-sign depth and leaf limits
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Maximum nesting depth for nested multi-signing (featureNestedMultiSign). */
|
||||||
|
constexpr int nestedMultiSignMaxDepth = 4;
|
||||||
|
|
||||||
|
/** Maximum nesting depth when nested multi-signing is disabled (flat only). */
|
||||||
|
constexpr int legacyMultiSignMaxDepth = 1;
|
||||||
|
|
||||||
|
/** Maximum total leaf signers across the entire nested tree.
|
||||||
|
Bounds worst-case signature verification cost. Only enforced when
|
||||||
|
featureNestedMultiSign is enabled; flat signing is already capped by the
|
||||||
|
per-array multisign limit.
|
||||||
|
*/
|
||||||
|
constexpr std::size_t nestedMultiSignMaxLeafSigners = 64;
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Multi-sign signer entry helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Count populated fields in an STObject.
|
||||||
|
STObject::getCount() includes template slots for optional fields that are
|
||||||
|
not present. Signer shape validation needs only populated fields.
|
||||||
|
*/
|
||||||
|
inline std::size_t
|
||||||
|
countPresentFields(STObject const& obj)
|
||||||
|
{
|
||||||
|
std::size_t count = 0;
|
||||||
|
for (auto const& field : obj)
|
||||||
|
{
|
||||||
|
if (field.getSType() != STI_NOTPRESENT)
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A leaf signer has Account + SigningPubKey + TxnSignature only. */
|
||||||
|
inline bool
|
||||||
|
isLeafSigner(STObject const& signer)
|
||||||
|
{
|
||||||
|
return signer.isFieldPresent(sfAccount) &&
|
||||||
|
signer.isFieldPresent(sfSigningPubKey) &&
|
||||||
|
signer.isFieldPresent(sfTxnSignature) &&
|
||||||
|
!signer.isFieldPresent(sfSigners) && countPresentFields(signer) == 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A nested signer has Account + Signers only. */
|
||||||
|
inline bool
|
||||||
|
isNestedSigner(STObject const& signer)
|
||||||
|
{
|
||||||
|
return signer.isFieldPresent(sfAccount) &&
|
||||||
|
signer.isFieldPresent(sfSigners) &&
|
||||||
|
!signer.isFieldPresent(sfSigningPubKey) &&
|
||||||
|
!signer.isFieldPresent(sfTxnSignature) &&
|
||||||
|
countPresentFields(signer) == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when a signer entry is either a valid leaf or a valid nested signer. */
|
||||||
|
inline bool
|
||||||
|
isValidSignerEntry(STObject const& signer)
|
||||||
|
{
|
||||||
|
return isLeafSigner(signer) || isNestedSigner(signer);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||||
// in include/xrpl/protocol/Feature.h.
|
// in include/xrpl/protocol/Feature.h.
|
||||||
|
|
||||||
|
XRPL_FEATURE(NestedMultiSign, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
XRPL_FEATURE(IOURewardClaim, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FEATURE(IOURewardClaim, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
XRPL_FIX (IOULockedBalanceInvariant, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FIX (IOULockedBalanceInvariant, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
XRPL_FIX (ImportIssuer, Supported::yes, VoteBehavior::DefaultYes)
|
XRPL_FIX (ImportIssuer, Supported::yes, VoteBehavior::DefaultYes)
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ InnerObjectFormats::InnerObjectFormats()
|
|||||||
sfSigner.getCode(),
|
sfSigner.getCode(),
|
||||||
{
|
{
|
||||||
{sfAccount, soeREQUIRED},
|
{sfAccount, soeREQUIRED},
|
||||||
{sfSigningPubKey, soeREQUIRED},
|
{sfSigningPubKey, soeOPTIONAL},
|
||||||
{sfTxnSignature, soeREQUIRED},
|
{sfTxnSignature, soeOPTIONAL},
|
||||||
|
{sfSigners, soeOPTIONAL},
|
||||||
});
|
});
|
||||||
|
|
||||||
add(sfMajority.jsonName,
|
add(sfMajority.jsonName,
|
||||||
|
|||||||
@@ -370,11 +370,6 @@ STTx::checkMultiSign(
|
|||||||
|
|
||||||
STArray const& signers{getFieldArray(sfSigners)};
|
STArray const& signers{getFieldArray(sfSigners)};
|
||||||
|
|
||||||
// There are well known bounds that the number of signers must be within.
|
|
||||||
if (signers.size() < minMultiSigners ||
|
|
||||||
signers.size() > maxMultiSigners(&rules))
|
|
||||||
return Unexpected("Invalid Signers array size.");
|
|
||||||
|
|
||||||
// We can ease the computational load inside the loop a bit by
|
// We can ease the computational load inside the loop a bit by
|
||||||
// pre-constructing part of the data that we hash. Fill a Serializer
|
// pre-constructing part of the data that we hash. Fill a Serializer
|
||||||
// with the stuff that stays constant from signature to signature.
|
// with the stuff that stays constant from signature to signature.
|
||||||
@@ -387,64 +382,117 @@ STTx::checkMultiSign(
|
|||||||
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
|
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
|
||||||
(requireCanonicalSig == RequireFullyCanonicalSig::yes);
|
(requireCanonicalSig == RequireFullyCanonicalSig::yes);
|
||||||
|
|
||||||
// Signers must be in sorted order by AccountID.
|
|
||||||
AccountID lastAccountID(beast::zero);
|
|
||||||
|
|
||||||
bool const isWildcardNetwork =
|
bool const isWildcardNetwork =
|
||||||
isFieldPresent(sfNetworkID) && getFieldU32(sfNetworkID) == 65535;
|
isFieldPresent(sfNetworkID) && getFieldU32(sfNetworkID) == 65535;
|
||||||
|
|
||||||
for (auto const& signer : signers)
|
// Set max depth and leaf cap based on feature flag.
|
||||||
{
|
bool const nested = rules.enabled(featureNestedMultiSign);
|
||||||
auto const accountID = signer.getAccountID(sfAccount);
|
int const maxDepth =
|
||||||
|
nested ? nestedMultiSignMaxDepth : legacyMultiSignMaxDepth;
|
||||||
|
std::size_t const maxLeafSigners =
|
||||||
|
nested ? nestedMultiSignMaxLeafSigners : maxMultiSigners(&rules);
|
||||||
|
std::size_t totalLeafSigners = 0;
|
||||||
|
|
||||||
// The account owner may not multisign for themselves.
|
// Define recursive lambda for checking signatures at any depth.
|
||||||
if (accountID == txnAccountID)
|
std::function<Expected<void, std::string>(
|
||||||
return Unexpected("Invalid multisigner.");
|
STArray const&, AccountID const&, int)>
|
||||||
|
checkSignersArray;
|
||||||
|
|
||||||
// No duplicate signers allowed.
|
checkSignersArray = [&](STArray const& signersArray,
|
||||||
if (lastAccountID == accountID)
|
[[maybe_unused]] AccountID const& parentAccountID,
|
||||||
return Unexpected("Duplicate Signers not allowed.");
|
int depth) -> Expected<void, std::string> {
|
||||||
|
// Check depth limit.
|
||||||
|
if (depth > maxDepth)
|
||||||
|
return Unexpected("Multi-signing depth limit exceeded.");
|
||||||
|
|
||||||
// Accounts must be in order by account ID. No duplicates allowed.
|
// There are well known bounds that the number of signers must be
|
||||||
if (lastAccountID > accountID)
|
// within.
|
||||||
return Unexpected("Unsorted Signers array.");
|
if (signersArray.size() < minMultiSigners ||
|
||||||
|
signersArray.size() > maxMultiSigners(&rules))
|
||||||
|
return Unexpected("Invalid Signers array size.");
|
||||||
|
|
||||||
// The next signature must be greater than this one.
|
// Signers must be in sorted order by AccountID.
|
||||||
lastAccountID = accountID;
|
AccountID lastAccountID(beast::zero);
|
||||||
|
|
||||||
// Verify the signature.
|
for (auto const& signer : signersArray)
|
||||||
bool validSig = false;
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
Serializer s = dataStart;
|
auto const accountID = signer.getAccountID(sfAccount);
|
||||||
finishMultiSigningData(accountID, s);
|
|
||||||
|
|
||||||
auto spk = signer.getFieldVL(sfSigningPubKey);
|
// The account owner may not multisign for themselves.
|
||||||
|
if (accountID == txnAccountID)
|
||||||
|
return Unexpected("Invalid multisigner.");
|
||||||
|
|
||||||
if (publicKeyType(makeSlice(spk)))
|
// No duplicate signers allowed.
|
||||||
|
if (lastAccountID == accountID)
|
||||||
|
return Unexpected("Duplicate Signers not allowed.");
|
||||||
|
|
||||||
|
// Accounts must be in order by account ID. No duplicates allowed.
|
||||||
|
if (lastAccountID > accountID)
|
||||||
|
return Unexpected("Unsorted Signers array.");
|
||||||
|
|
||||||
|
// The next signature must be greater than this one.
|
||||||
|
lastAccountID = accountID;
|
||||||
|
|
||||||
|
if (isNestedSigner(signer))
|
||||||
{
|
{
|
||||||
Blob const signature = signer.getFieldVL(sfTxnSignature);
|
if (!nested)
|
||||||
|
return Unexpected("FeatureNestedMultiSign is disabled");
|
||||||
|
|
||||||
// wildcard network gets a free pass
|
STArray const& nestedSigners = signer.getFieldArray(sfSigners);
|
||||||
validSig = isWildcardNetwork ||
|
auto result =
|
||||||
verify(PublicKey(makeSlice(spk)),
|
checkSignersArray(nestedSigners, accountID, depth + 1);
|
||||||
s.slice(),
|
if (!result)
|
||||||
makeSlice(signature),
|
return result;
|
||||||
fullyCanonical);
|
}
|
||||||
|
else if (isLeafSigner(signer))
|
||||||
|
{
|
||||||
|
if (++totalLeafSigners > maxLeafSigners)
|
||||||
|
return Unexpected(std::string("Too many leaf signers."));
|
||||||
|
|
||||||
|
bool validSig = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Serializer s = dataStart;
|
||||||
|
finishMultiSigningData(accountID, s);
|
||||||
|
|
||||||
|
auto spk = signer.getFieldVL(sfSigningPubKey);
|
||||||
|
|
||||||
|
if (publicKeyType(makeSlice(spk)))
|
||||||
|
{
|
||||||
|
Blob const signature =
|
||||||
|
signer.getFieldVL(sfTxnSignature);
|
||||||
|
|
||||||
|
// wildcard network gets a free pass
|
||||||
|
validSig = isWildcardNetwork ||
|
||||||
|
verify(PublicKey(makeSlice(spk)),
|
||||||
|
s.slice(),
|
||||||
|
makeSlice(signature),
|
||||||
|
fullyCanonical);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (std::exception const&)
|
||||||
|
{
|
||||||
|
// We assume any problem lies with the signature.
|
||||||
|
validSig = false;
|
||||||
|
}
|
||||||
|
if (!validSig)
|
||||||
|
return Unexpected(
|
||||||
|
std::string("Invalid signature on account ") +
|
||||||
|
toBase58(accountID) + ".");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Unexpected(
|
||||||
|
std::string("Malformed signer entry for account ") +
|
||||||
|
toBase58(accountID) + ".");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (std::exception const&)
|
|
||||||
{
|
return {};
|
||||||
// We assume any problem lies with the signature.
|
};
|
||||||
validSig = false;
|
|
||||||
}
|
// Start the recursive check at depth 1.
|
||||||
if (!validSig)
|
return checkSignersArray(signers, txnAccountID, 1);
|
||||||
return Unexpected(
|
|
||||||
std::string("Invalid signature on account ") +
|
|
||||||
toBase58(accountID) + ".");
|
|
||||||
}
|
|
||||||
// All signatures verified.
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -66,15 +66,45 @@ signers(Account const& account, none_t)
|
|||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
msig::msig(std::vector<msig::Reg> signers_) : signers(std::move(signers_))
|
// Helper function to recursively sort nested signers
|
||||||
|
void
|
||||||
|
sortSignersRecursive(std::vector<msig::SignerPtr>& signers)
|
||||||
{
|
{
|
||||||
// Signatures must be applied in sorted order.
|
// Sort current level by account ID
|
||||||
std::sort(
|
std::sort(
|
||||||
signers.begin(),
|
signers.begin(),
|
||||||
signers.end(),
|
signers.end(),
|
||||||
[](msig::Reg const& lhs, msig::Reg const& rhs) {
|
[](msig::SignerPtr const& lhs, msig::SignerPtr const& rhs) {
|
||||||
return lhs.acct.id() < rhs.acct.id();
|
return lhs->id() < rhs->id();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Recursively sort nested signers for each signer at this level
|
||||||
|
for (auto& signer : signers)
|
||||||
|
{
|
||||||
|
if (signer->isNested() && !signer->nested.empty())
|
||||||
|
{
|
||||||
|
sortSignersRecursive(signer->nested);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msig::msig(std::vector<msig::SignerPtr> signers_) : signers(std::move(signers_))
|
||||||
|
{
|
||||||
|
// Recursively sort all signers at all nesting levels
|
||||||
|
// This ensures account IDs are in strictly ascending order at each level
|
||||||
|
sortSignersRecursive(signers);
|
||||||
|
}
|
||||||
|
|
||||||
|
msig::msig(std::vector<msig::Reg> signers_)
|
||||||
|
{
|
||||||
|
// Convert Reg vector to SignerPtr vector for backward compatibility
|
||||||
|
signers.reserve(signers_.size());
|
||||||
|
for (auto const& s : signers_)
|
||||||
|
signers.push_back(s.toSigner());
|
||||||
|
|
||||||
|
// Recursively sort all signers at all nesting levels
|
||||||
|
// This ensures account IDs are in strictly ascending order at each level
|
||||||
|
sortSignersRecursive(signers);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -93,19 +123,47 @@ msig::operator()(Env& env, JTx& jt) const
|
|||||||
env.test.log << pretty(jtx.jv) << std::endl;
|
env.test.log << pretty(jtx.jv) << std::endl;
|
||||||
Rethrow();
|
Rethrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursive function to build signer JSON
|
||||||
|
std::function<Json::Value(SignerPtr const&)> buildSignerJson;
|
||||||
|
buildSignerJson = [&](SignerPtr const& signer) -> Json::Value {
|
||||||
|
Json::Value jo;
|
||||||
|
jo[jss::Account] = signer->acct.human();
|
||||||
|
|
||||||
|
if (signer->isNested())
|
||||||
|
{
|
||||||
|
// For nested signers, we use the already-sorted nested vector
|
||||||
|
// (sorted during construction via sortSignersRecursive)
|
||||||
|
// This ensures account IDs are in strictly ascending order
|
||||||
|
auto& subJs = jo[sfSigners.getJsonName()];
|
||||||
|
for (std::size_t i = 0; i < signer->nested.size(); ++i)
|
||||||
|
{
|
||||||
|
auto& subJo = subJs[i][sfSigner.getJsonName()];
|
||||||
|
subJo = buildSignerJson(signer->nested[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is a leaf signer - add signature
|
||||||
|
jo[jss::SigningPubKey] = strHex(signer->sig.pk().slice());
|
||||||
|
|
||||||
|
Serializer ss{buildMultiSigningData(*st, signer->acct.id())};
|
||||||
|
auto const sig = ripple::sign(
|
||||||
|
*publicKeyType(signer->sig.pk().slice()),
|
||||||
|
signer->sig.sk(),
|
||||||
|
ss.slice());
|
||||||
|
jo[sfTxnSignature.getJsonName()] =
|
||||||
|
strHex(Slice{sig.data(), sig.size()});
|
||||||
|
}
|
||||||
|
|
||||||
|
return jo;
|
||||||
|
};
|
||||||
|
|
||||||
auto& js = jtx[sfSigners.getJsonName()];
|
auto& js = jtx[sfSigners.getJsonName()];
|
||||||
for (std::size_t i = 0; i < mySigners.size(); ++i)
|
for (std::size_t i = 0; i < mySigners.size(); ++i)
|
||||||
{
|
{
|
||||||
auto const& e = mySigners[i];
|
|
||||||
auto& jo = js[i][sfSigner.getJsonName()];
|
auto& jo = js[i][sfSigner.getJsonName()];
|
||||||
jo[jss::Account] = e.acct.human();
|
jo = buildSignerJson(mySigners[i]);
|
||||||
jo[jss::SigningPubKey] = strHex(e.sig.pk().slice());
|
|
||||||
|
|
||||||
Serializer ss{buildMultiSigningData(*st, e.acct.id())};
|
|
||||||
auto const sig = ripple::sign(
|
|
||||||
*publicKeyType(e.sig.pk().slice()), e.sig.sk(), ss.slice());
|
|
||||||
jo[sfTxnSignature.getJsonName()] =
|
|
||||||
strHex(Slice{sig.data(), sig.size()});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include <test/jtx/tags.h>
|
#include <test/jtx/tags.h>
|
||||||
#include <concepts>
|
#include <concepts>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
@@ -66,6 +67,48 @@ signers(Account const& account, none_t);
|
|||||||
class msig
|
class msig
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
// Recursive signer structure
|
||||||
|
struct Signer
|
||||||
|
{
|
||||||
|
Account acct;
|
||||||
|
Account sig; // For leaf signers (same as acct for master key)
|
||||||
|
std::vector<std::shared_ptr<Signer>> nested; // For nested signers
|
||||||
|
|
||||||
|
// Leaf signer constructor (regular signing)
|
||||||
|
Signer(Account const& masterSig) : acct(masterSig), sig(masterSig)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaf signer constructor (with different signing key)
|
||||||
|
Signer(Account const& acct_, Account const& regularSig)
|
||||||
|
: acct(acct_), sig(regularSig)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested signer constructor
|
||||||
|
Signer(
|
||||||
|
Account const& acct_,
|
||||||
|
std::vector<std::shared_ptr<Signer>> nested_)
|
||||||
|
: acct(acct_), sig(acct_), nested(std::move(nested_))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
isNested() const
|
||||||
|
{
|
||||||
|
return !nested.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountID
|
||||||
|
id() const
|
||||||
|
{
|
||||||
|
return acct.id();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using SignerPtr = std::shared_ptr<Signer>;
|
||||||
|
|
||||||
|
// For backward compatibility
|
||||||
struct Reg
|
struct Reg
|
||||||
{
|
{
|
||||||
Account acct;
|
Account acct;
|
||||||
@@ -74,16 +117,13 @@ public:
|
|||||||
Reg(Account const& masterSig) : acct(masterSig), sig(masterSig)
|
Reg(Account const& masterSig) : acct(masterSig), sig(masterSig)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Reg(Account const& acct_, Account const& regularSig)
|
Reg(Account const& acct_, Account const& regularSig)
|
||||||
: acct(acct_), sig(regularSig)
|
: acct(acct_), sig(regularSig)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Reg(char const* masterSig) : acct(masterSig), sig(masterSig)
|
Reg(char const* masterSig) : acct(masterSig), sig(masterSig)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Reg(char const* acct_, char const* regularSig)
|
Reg(char const* acct_, char const* regularSig)
|
||||||
: acct(acct_), sig(regularSig)
|
: acct(acct_), sig(regularSig)
|
||||||
{
|
{
|
||||||
@@ -94,13 +134,32 @@ public:
|
|||||||
{
|
{
|
||||||
return acct < rhs.acct;
|
return acct < rhs.acct;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert to Signer
|
||||||
|
SignerPtr
|
||||||
|
toSigner() const
|
||||||
|
{
|
||||||
|
return std::make_shared<Signer>(acct, sig);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<Reg> signers;
|
std::vector<SignerPtr> signers;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
// Initializer list constructor - resolves brace-init ambiguity
|
||||||
|
msig(std::initializer_list<SignerPtr> signers_)
|
||||||
|
: msig(std::vector<SignerPtr>(signers_))
|
||||||
|
{
|
||||||
|
// handled by :
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct constructor with SignerPtr vector
|
||||||
|
explicit msig(std::vector<SignerPtr> signers_);
|
||||||
|
|
||||||
|
// Backward compatibility constructor
|
||||||
msig(std::vector<Reg> signers_);
|
msig(std::vector<Reg> signers_);
|
||||||
|
|
||||||
|
// Variadic constructor for backward compatibility
|
||||||
template <class AccountType, class... Accounts>
|
template <class AccountType, class... Accounts>
|
||||||
requires std::convertible_to<AccountType, Reg>
|
requires std::convertible_to<AccountType, Reg>
|
||||||
explicit msig(AccountType&& a0, Accounts&&... aN)
|
explicit msig(AccountType&& a0, Accounts&&... aN)
|
||||||
@@ -114,6 +173,30 @@ public:
|
|||||||
operator()(Env&, JTx& jt) const;
|
operator()(Env&, JTx& jt) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper functions to create signers - renamed to avoid conflict with sig()
|
||||||
|
// transaction modifier
|
||||||
|
inline msig::SignerPtr
|
||||||
|
msigner(Account const& acct)
|
||||||
|
{
|
||||||
|
return std::make_shared<msig::Signer>(acct);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline msig::SignerPtr
|
||||||
|
msigner(Account const& acct, Account const& signingKey)
|
||||||
|
{
|
||||||
|
return std::make_shared<msig::Signer>(acct, signingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create nested signer with initializer list
|
||||||
|
template <typename... Args>
|
||||||
|
inline msig::SignerPtr
|
||||||
|
msigner(Account const& acct, Args&&... args)
|
||||||
|
{
|
||||||
|
std::vector<msig::SignerPtr> nested;
|
||||||
|
(nested.push_back(std::forward<Args>(args)), ...);
|
||||||
|
return std::make_shared<msig::Signer>(acct, std::move(nested));
|
||||||
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
/** The number of signer lists matches. */
|
/** The number of signer lists matches. */
|
||||||
|
|||||||
@@ -1798,30 +1798,32 @@ public:
|
|||||||
|
|
||||||
// This lambda contains the bulk of the test code.
|
// This lambda contains the bulk of the test code.
|
||||||
auto testMalformedSigningAccount =
|
auto testMalformedSigningAccount =
|
||||||
[this, &txn](STObject const& signer, bool expectPass) {
|
[this, &txn](
|
||||||
// Create SigningAccounts array.
|
STObject const& signer, bool expectPass) -> bool /* passed */ {
|
||||||
STArray signers(sfSigners, 1);
|
// Create SigningAccounts array.
|
||||||
signers.push_back(signer);
|
STArray signers(sfSigners, 1);
|
||||||
|
signers.push_back(signer);
|
||||||
|
|
||||||
// Insert signers into transaction.
|
// Insert signers into transaction.
|
||||||
STTx tempTxn(txn);
|
STTx tempTxn(txn);
|
||||||
tempTxn.setFieldArray(sfSigners, signers);
|
tempTxn.setFieldArray(sfSigners, signers);
|
||||||
|
|
||||||
Serializer rawTxn;
|
Serializer rawTxn;
|
||||||
tempTxn.add(rawTxn);
|
tempTxn.add(rawTxn);
|
||||||
SerialIter sit(rawTxn.slice());
|
SerialIter sit(rawTxn.slice());
|
||||||
bool serialized = false;
|
bool serialized = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
STTx copy(sit);
|
STTx copy(sit);
|
||||||
serialized = true;
|
serialized = true;
|
||||||
}
|
}
|
||||||
catch (std::exception const&)
|
catch (std::exception const&)
|
||||||
{
|
{
|
||||||
; // If it threw then serialization failed.
|
; // If it threw then serialization failed.
|
||||||
}
|
}
|
||||||
BEAST_EXPECT(serialized == expectPass);
|
BEAST_EXPECT(serialized == expectPass);
|
||||||
};
|
return serialized == expectPass;
|
||||||
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test case 1. Make a valid Signer object.
|
// Test case 1. Make a valid Signer object.
|
||||||
@@ -1831,12 +1833,21 @@ public:
|
|||||||
soTest1.setFieldVL(sfTxnSignature, saMultiSignature);
|
soTest1.setFieldVL(sfTxnSignature, saMultiSignature);
|
||||||
testMalformedSigningAccount(soTest1, true);
|
testMalformedSigningAccount(soTest1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Test case 2. Omit sfSigningPubKey from SigningAccount.
|
// Test case 2. Omit sfSigningPubKey from SigningAccount.
|
||||||
|
// With nested multi-sign, sfSigningPubKey is optional in the
|
||||||
|
// template, so serialization succeeds. The signer-shape helpers
|
||||||
|
// still reject {Account + TxnSignature} as neither leaf nor nested.
|
||||||
STObject soTest2(sfSigner);
|
STObject soTest2(sfSigner);
|
||||||
soTest2.setAccountID(sfAccount, id2);
|
soTest2.setAccountID(sfAccount, id2);
|
||||||
soTest2.setFieldVL(sfTxnSignature, saMultiSignature);
|
soTest2.setFieldVL(sfTxnSignature, saMultiSignature);
|
||||||
testMalformedSigningAccount(soTest2, false);
|
testMalformedSigningAccount(soTest2, true);
|
||||||
|
|
||||||
|
soTest2.applyTemplateFromSField(sfSigner);
|
||||||
|
BEAST_EXPECT(!isLeafSigner(soTest2));
|
||||||
|
BEAST_EXPECT(!isNestedSigner(soTest2));
|
||||||
|
BEAST_EXPECT(!isValidSignerEntry(soTest2));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// Test case 3. Extra sfAmount in SigningAccount.
|
// Test case 3. Extra sfAmount in SigningAccount.
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ multi_runner_child::run_multi(Pred pred)
|
|||||||
{
|
{
|
||||||
if (!pred(*t))
|
if (!pred(*t))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
failed = run(*t) || failed;
|
failed = run(*t) || failed;
|
||||||
|
|||||||
@@ -38,8 +38,12 @@
|
|||||||
#include <xrpl/protocol/Indexes.h>
|
#include <xrpl/protocol/Indexes.h>
|
||||||
#include <xrpl/protocol/Protocol.h>
|
#include <xrpl/protocol/Protocol.h>
|
||||||
#include <xrpl/protocol/STAccount.h>
|
#include <xrpl/protocol/STAccount.h>
|
||||||
|
#include <xrpl/protocol/STTx.h>
|
||||||
#include <xrpl/protocol/UintTypes.h>
|
#include <xrpl/protocol/UintTypes.h>
|
||||||
|
#include <functional>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
@@ -317,8 +321,36 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
|
|||||||
|
|
||||||
// Each signer adds one more baseFee to the minimum required fee
|
// Each signer adds one more baseFee to the minimum required fee
|
||||||
// for the transaction.
|
// for the transaction.
|
||||||
std::size_t const signerCount =
|
std::size_t signerCount = 0;
|
||||||
tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 0;
|
if (tx.isFieldPresent(sfSigners))
|
||||||
|
{
|
||||||
|
// Depth guard to prevent stack overflow from malicious deep nesting.
|
||||||
|
int const maxDepth = view.rules().enabled(featureNestedMultiSign)
|
||||||
|
? nestedMultiSignMaxDepth
|
||||||
|
: legacyMultiSignMaxDepth;
|
||||||
|
|
||||||
|
std::function<std::size_t(STArray const&, int)> countSigners;
|
||||||
|
|
||||||
|
countSigners = [&](STArray const& signers, int depth) -> std::size_t {
|
||||||
|
if (depth > maxDepth)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
std::size_t count = 0;
|
||||||
|
|
||||||
|
for (auto const& signer : signers)
|
||||||
|
{
|
||||||
|
if (isNestedSigner(signer))
|
||||||
|
count += countSigners(
|
||||||
|
signer.getFieldArray(sfSigners), depth + 1);
|
||||||
|
else if (isLeafSigner(signer))
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
signerCount = countSigners(tx.getFieldArray(sfSigners), 1);
|
||||||
|
}
|
||||||
|
|
||||||
XRPAmount hookExecutionFee{0};
|
XRPAmount hookExecutionFee{0};
|
||||||
uint64_t burden{1};
|
uint64_t burden{1};
|
||||||
@@ -972,167 +1004,273 @@ NotTEC
|
|||||||
Transactor::checkMultiSign(PreclaimContext const& ctx)
|
Transactor::checkMultiSign(PreclaimContext const& ctx)
|
||||||
{
|
{
|
||||||
auto const id = ctx.tx.getAccountID(sfAccount);
|
auto const id = ctx.tx.getAccountID(sfAccount);
|
||||||
// Get mTxnAccountID's SignerList and Quorum.
|
|
||||||
std::shared_ptr<STLedgerEntry const> sleAccountSigners =
|
|
||||||
ctx.view.read(keylet::signers(id));
|
|
||||||
// If the signer list doesn't exist the account is not multi-signing.
|
|
||||||
if (!sleAccountSigners)
|
|
||||||
{
|
|
||||||
JLOG(ctx.j.trace())
|
|
||||||
<< "applyTransaction: Invalid: Not a multi-signing account.";
|
|
||||||
return tefNOT_MULTI_SIGNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have plans to support multiple SignerLists in the future. The
|
bool const allowNested = ctx.view.rules().enabled(featureNestedMultiSign);
|
||||||
// presence and defaulted value of the SignerListID field will enable that.
|
int const maxDepth =
|
||||||
XRPL_ASSERT(
|
allowNested ? nestedMultiSignMaxDepth : legacyMultiSignMaxDepth;
|
||||||
sleAccountSigners->isFieldPresent(sfSignerListID),
|
|
||||||
"ripple::Transactor::checkMultiSign : has signer list ID");
|
|
||||||
XRPL_ASSERT(
|
|
||||||
sleAccountSigners->getFieldU32(sfSignerListID) == 0,
|
|
||||||
"ripple::Transactor::checkMultiSign : signer list ID is 0");
|
|
||||||
|
|
||||||
auto accountSigners =
|
// Nested multi-sign is dynamic account delegation: a parent SignerList
|
||||||
SignerEntries::deserialize(*sleAccountSigners, ctx.j, "ledger");
|
// authorizes signer accounts, not a frozen set of leaf keys. A nested
|
||||||
if (!accountSigners)
|
// signer contributes when that signer account's current on-ledger
|
||||||
return accountSigners.error();
|
// SignerList is satisfied by the transaction evidence.
|
||||||
|
//
|
||||||
|
// ancestors tracks the current proof path. An authorized signer entry that
|
||||||
|
// points back to an ancestor is unavailable on that path and may cause the
|
||||||
|
// local quorum to be cycle-adjusted below.
|
||||||
|
std::function<NotTEC(
|
||||||
|
AccountID const&, STArray const&, int, std::set<AccountID>)>
|
||||||
|
validateSigners;
|
||||||
|
|
||||||
// Get the array of transaction signers.
|
validateSigners = [&](AccountID const& acc,
|
||||||
STArray const& txSigners(ctx.tx.getFieldArray(sfSigners));
|
STArray const& signers,
|
||||||
|
int depth,
|
||||||
|
std::set<AccountID> ancestors) -> NotTEC {
|
||||||
|
// Cycle detection is handled per authorized edge in the loop below,
|
||||||
|
// rather than by failing the delegated account outright.
|
||||||
|
|
||||||
// Walk the accountSigners performing a variety of checks and see if
|
if (depth > maxDepth)
|
||||||
// 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 iter = accountSigners->begin();
|
|
||||||
for (auto const& txSigner : txSigners)
|
|
||||||
{
|
|
||||||
AccountID const txSignerAcctID = txSigner.getAccountID(sfAccount);
|
|
||||||
|
|
||||||
// Attempt to match the SignerEntry with a Signer;
|
|
||||||
while (iter->account < txSignerAcctID)
|
|
||||||
{
|
{
|
||||||
if (++iter == accountSigners->end())
|
JLOG(ctx.j.trace())
|
||||||
|
<< "checkMultiSign: Multi-signing depth limit exceeded at "
|
||||||
|
<< depth << " (max=" << maxDepth << ")";
|
||||||
|
return temMALFORMED;
|
||||||
|
}
|
||||||
|
|
||||||
|
ancestors.insert(acc);
|
||||||
|
|
||||||
|
// Get the SignerList for the account we're validating signers for.
|
||||||
|
std::shared_ptr<STLedgerEntry const> sleAllowedSigners =
|
||||||
|
ctx.view.read(keylet::signers(acc));
|
||||||
|
|
||||||
|
if (!sleAllowedSigners)
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.trace()) << "checkMultiSign: Account " << acc
|
||||||
|
<< " not set up for multi-signing.";
|
||||||
|
return tefNOT_MULTI_SIGNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have plans to support multiple SignerLists in the future. The
|
||||||
|
// presence and defaulted value of the SignerListID field will enable
|
||||||
|
// that.
|
||||||
|
XRPL_ASSERT(
|
||||||
|
sleAllowedSigners->isFieldPresent(sfSignerListID),
|
||||||
|
"ripple::Transactor::checkMultiSign : has signer list ID");
|
||||||
|
XRPL_ASSERT(
|
||||||
|
sleAllowedSigners->getFieldU32(sfSignerListID) == 0,
|
||||||
|
"ripple::Transactor::checkMultiSign : signer list ID is 0");
|
||||||
|
|
||||||
|
uint32_t const quorum = sleAllowedSigners->getFieldU32(sfSignerQuorum);
|
||||||
|
uint32_t sum{0};
|
||||||
|
|
||||||
|
auto allowedSigners =
|
||||||
|
SignerEntries::deserialize(*sleAllowedSigners, ctx.j, "ledger");
|
||||||
|
if (!allowedSigners)
|
||||||
|
return allowedSigners.error();
|
||||||
|
|
||||||
|
// Build lookup map for signer validation and weight retrieval.
|
||||||
|
std::map<AccountID, uint16_t> signerWeights;
|
||||||
|
uint32_t totalWeight{0}, cyclicWeight{0};
|
||||||
|
for (auto const& entry : *allowedSigners)
|
||||||
|
{
|
||||||
|
signerWeights[entry.account] = entry.weight;
|
||||||
|
totalWeight += entry.weight;
|
||||||
|
if (ancestors.count(entry.account))
|
||||||
|
cyclicWeight += entry.weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the signers array, validating each signer. Signers must be in
|
||||||
|
// strict ascending order for consensus.
|
||||||
|
std::optional<AccountID> prevSigner;
|
||||||
|
|
||||||
|
for (auto const& signerEntry : signers)
|
||||||
|
{
|
||||||
|
AccountID const signer = signerEntry.getAccountID(sfAccount);
|
||||||
|
|
||||||
|
if (prevSigner && signer <= *prevSigner)
|
||||||
{
|
{
|
||||||
JLOG(ctx.j.trace())
|
JLOG(ctx.j.trace())
|
||||||
<< "applyTransaction: Invalid SigningAccount.Account.";
|
<< "checkMultiSign: Signers not in strict ascending order: "
|
||||||
|
<< signer << " <= " << *prevSigner;
|
||||||
|
return temMALFORMED;
|
||||||
|
}
|
||||||
|
prevSigner = signer;
|
||||||
|
|
||||||
|
// Parent-edge authorization is checked before cycle skipping so an
|
||||||
|
// unauthorized cyclic entry is rejected, not silently ignored.
|
||||||
|
auto const weightIt = signerWeights.find(signer);
|
||||||
|
if (weightIt == signerWeights.end())
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.trace())
|
||||||
|
<< "checkMultiSign: Invalid signer " << signer
|
||||||
|
<< " not in signer list for " << acc;
|
||||||
return tefBAD_SIGNATURE;
|
return tefBAD_SIGNATURE;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (iter->account != txSignerAcctID)
|
|
||||||
{
|
|
||||||
// The SigningAccount is not in the SignerEntries.
|
|
||||||
JLOG(ctx.j.trace())
|
|
||||||
<< "applyTransaction: Invalid SigningAccount.Account.";
|
|
||||||
return tefBAD_SIGNATURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We found the SigningAccount in the list of valid signers. Now we
|
// The signer is authorized by acc, but is already in the current
|
||||||
// need to compute the accountID that is associated with the signer's
|
// proof path. Treat that cyclic ancestor edge as unavailable for
|
||||||
// public key.
|
// this path; do not recurse into it and do not count its weight.
|
||||||
auto const spk = txSigner.getFieldVL(sfSigningPubKey);
|
if (ancestors.count(signer))
|
||||||
|
|
||||||
if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk)))
|
|
||||||
{
|
|
||||||
JLOG(ctx.j.trace())
|
|
||||||
<< "checkMultiSign: signing public key type is unknown";
|
|
||||||
return tefBAD_SIGNATURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This ternary is only needed to handle `simulate`
|
|
||||||
XRPL_ASSERT(
|
|
||||||
(ctx.flags & tapDRY_RUN) || !spk.empty(),
|
|
||||||
"ripple::Transactor::checkMultiSign : non-empty signer or "
|
|
||||||
"simulation");
|
|
||||||
AccountID const signingAcctIDFromPubKey = spk.empty()
|
|
||||||
? txSignerAcctID
|
|
||||||
: calcAccountID(PublicKey(makeSlice(spk)));
|
|
||||||
|
|
||||||
// Verify that the signingAcctID and the signingAcctIDFromPubKey
|
|
||||||
// belong together. Here 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.
|
|
||||||
auto sleTxSignerRoot = ctx.view.read(keylet::account(txSignerAcctID));
|
|
||||||
|
|
||||||
if (signingAcctIDFromPubKey == txSignerAcctID)
|
|
||||||
{
|
|
||||||
// Either Phantom or Master. Phantoms automatically pass.
|
|
||||||
if (sleTxSignerRoot)
|
|
||||||
{
|
{
|
||||||
// Master Key. Account may not have asfDisableMaster set.
|
JLOG(ctx.j.trace())
|
||||||
std::uint32_t const signerAccountFlags =
|
<< "checkMultiSign: Skipping cyclic signer: " << signer;
|
||||||
sleTxSignerRoot->getFieldU32(sfFlags);
|
continue;
|
||||||
|
}
|
||||||
|
uint16_t const weight = weightIt->second;
|
||||||
|
|
||||||
if (signerAccountFlags & lsfDisableMaster)
|
if (isNestedSigner(signerEntry))
|
||||||
|
{
|
||||||
|
STArray const& nestedSigners =
|
||||||
|
signerEntry.getFieldArray(sfSigners);
|
||||||
|
NotTEC result = validateSigners(
|
||||||
|
signer, nestedSigners, depth + 1, ancestors);
|
||||||
|
if (!isTesSuccess(result))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
sum += weight;
|
||||||
|
|
||||||
|
JLOG(ctx.j.trace())
|
||||||
|
<< "checkMultiSign: Nested signer " << signer
|
||||||
|
<< " validated, weight=" << weight << ", depth=" << depth
|
||||||
|
<< ", sum=" << sum << "/" << quorum;
|
||||||
|
}
|
||||||
|
else if (isLeafSigner(signerEntry))
|
||||||
|
{
|
||||||
|
auto const spk = signerEntry.getFieldVL(sfSigningPubKey);
|
||||||
|
|
||||||
|
// spk being non-empty in non-simulate is checked in
|
||||||
|
// STTx::checkMultiSign.
|
||||||
|
if (!spk.empty() && !publicKeyType(makeSlice(spk)))
|
||||||
{
|
{
|
||||||
JLOG(ctx.j.trace())
|
JLOG(ctx.j.trace())
|
||||||
<< "applyTransaction: Signer:Account lsfDisableMaster.";
|
<< "checkMultiSign: Unknown public key type for signer "
|
||||||
return tefMASTER_DISABLED;
|
<< signer;
|
||||||
|
return tefBAD_SIGNATURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
XRPL_ASSERT(
|
||||||
|
(ctx.flags & tapDRY_RUN) || !spk.empty(),
|
||||||
|
"ripple::Transactor::checkMultiSign : non-empty signer or "
|
||||||
|
"simulation");
|
||||||
|
AccountID const signingAcctIDFromPubKey = spk.empty()
|
||||||
|
? signer
|
||||||
|
: calcAccountID(PublicKey(makeSlice(spk)));
|
||||||
|
|
||||||
|
auto sleTxSignerRoot = ctx.view.read(keylet::account(signer));
|
||||||
|
|
||||||
|
if (signingAcctIDFromPubKey == signer)
|
||||||
|
{
|
||||||
|
// Either Phantom or Master. Phantoms automatically pass.
|
||||||
|
if (sleTxSignerRoot)
|
||||||
|
{
|
||||||
|
// Master Key. Account may not have asfDisableMaster
|
||||||
|
// set.
|
||||||
|
std::uint32_t const signerAccountFlags =
|
||||||
|
sleTxSignerRoot->getFieldU32(sfFlags);
|
||||||
|
|
||||||
|
if (signerAccountFlags & lsfDisableMaster)
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.trace())
|
||||||
|
<< "checkMultiSign: Signer " << signer
|
||||||
|
<< " has lsfDisableMaster set.";
|
||||||
|
return tefMASTER_DISABLED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// May be a Regular Key. Let's find out.
|
||||||
|
if (!sleTxSignerRoot)
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.trace())
|
||||||
|
<< "checkMultiSign: Non-phantom signer " << signer
|
||||||
|
<< " lacks account root.";
|
||||||
|
return tefBAD_SIGNATURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sleTxSignerRoot->isFieldPresent(sfRegularKey))
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.trace()) << "checkMultiSign: Signer "
|
||||||
|
<< signer << " lacks RegularKey.";
|
||||||
|
return tefBAD_SIGNATURE;
|
||||||
|
}
|
||||||
|
if (signingAcctIDFromPubKey !=
|
||||||
|
sleTxSignerRoot->getAccountID(sfRegularKey))
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.trace())
|
||||||
|
<< "checkMultiSign: Signer " << signer
|
||||||
|
<< " pubkey doesn't match RegularKey.";
|
||||||
|
return tefBAD_SIGNATURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sum += weight;
|
||||||
|
|
||||||
|
JLOG(ctx.j.trace())
|
||||||
|
<< "checkMultiSign: Leaf signer " << signer
|
||||||
|
<< " validated, weight=" << weight << ", depth=" << depth
|
||||||
|
<< ", sum=" << sum << "/" << quorum;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.trace())
|
||||||
|
<< "checkMultiSign: Malformed signer entry for " << signer;
|
||||||
|
return tefBAD_SIGNATURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (cyclicWeight > totalWeight)
|
||||||
{
|
{
|
||||||
// May be a Regular Key. Let's find out.
|
JLOG(ctx.j.error()) << "checkMultiSign: Invariant violation for "
|
||||||
// Public key must hash to the account's regular key.
|
<< acc << ": cyclicWeight (" << cyclicWeight
|
||||||
if (!sleTxSignerRoot)
|
<< ") > totalWeight (" << totalWeight << ")";
|
||||||
{
|
return tefINTERNAL;
|
||||||
JLOG(ctx.j.trace()) << "applyTransaction: Non-phantom signer "
|
|
||||||
"lacks account root.";
|
|
||||||
return tefBAD_SIGNATURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sleTxSignerRoot->isFieldPresent(sfRegularKey))
|
|
||||||
{
|
|
||||||
JLOG(ctx.j.trace())
|
|
||||||
<< "applyTransaction: Account lacks RegularKey.";
|
|
||||||
return tefBAD_SIGNATURE;
|
|
||||||
}
|
|
||||||
if (signingAcctIDFromPubKey !=
|
|
||||||
sleTxSignerRoot->getAccountID(sfRegularKey))
|
|
||||||
{
|
|
||||||
JLOG(ctx.j.trace())
|
|
||||||
<< "applyTransaction: Account doesn't match RegularKey.";
|
|
||||||
return tefBAD_SIGNATURE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// The signer is legitimate. Add their weight toward the quorum.
|
|
||||||
weightSum += iter->weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cannot perform transaction if quorum is not met.
|
// Dynamic delegation still requires the delegated account's own policy
|
||||||
if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum))
|
// to be satisfied. The only adjustment is for authorized ancestor edges
|
||||||
|
// that cannot be used without circular proof. If those cyclic edges
|
||||||
|
// make the configured quorum unreachable, require all remaining
|
||||||
|
// non-cyclic weight.
|
||||||
|
uint32_t cycleAdjustedQuorum = quorum;
|
||||||
|
uint32_t const maxAchievable = totalWeight - cyclicWeight;
|
||||||
|
|
||||||
|
if (cyclicWeight > 0 && maxAchievable < quorum)
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.warn()) << "checkMultiSign: Cycle-adjusted quorum for "
|
||||||
|
<< acc << ": " << quorum << " -> "
|
||||||
|
<< maxAchievable << " (total=" << totalWeight
|
||||||
|
<< ", cyclic=" << cyclicWeight << ")";
|
||||||
|
cycleAdjustedQuorum = maxAchievable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cycleAdjustedQuorum == 0)
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.warn()) << "checkMultiSign: All signers for " << acc
|
||||||
|
<< " are cyclic - no valid signing path exists.";
|
||||||
|
return tefBAD_QUORUM;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sum < cycleAdjustedQuorum)
|
||||||
|
{
|
||||||
|
JLOG(ctx.j.trace()) << "checkMultiSign: Quorum not met for " << acc
|
||||||
|
<< " at depth " << depth << " (sum=" << sum
|
||||||
|
<< ", required=" << cycleAdjustedQuorum << ")";
|
||||||
|
return tefBAD_QUORUM;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tesSUCCESS;
|
||||||
|
};
|
||||||
|
|
||||||
|
STArray const& entries(ctx.tx.getFieldArray(sfSigners));
|
||||||
|
|
||||||
|
NotTEC result = validateSigners(id, entries, 1, {});
|
||||||
|
if (!isTesSuccess(result))
|
||||||
{
|
{
|
||||||
JLOG(ctx.j.trace())
|
JLOG(ctx.j.trace())
|
||||||
<< "applyTransaction: Signers failed to meet quorum.";
|
<< "checkMultiSign: Validation failed with " << transToken(result);
|
||||||
return tefBAD_QUORUM;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Met the quorum. Continue.
|
|
||||||
return tesSUCCESS;
|
return tesSUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,11 @@
|
|||||||
#include <xrpl/protocol/RPCErr.h>
|
#include <xrpl/protocol/RPCErr.h>
|
||||||
#include <xrpl/protocol/STAccount.h>
|
#include <xrpl/protocol/STAccount.h>
|
||||||
#include <xrpl/protocol/STParsedJSON.h>
|
#include <xrpl/protocol/STParsedJSON.h>
|
||||||
|
#include <xrpl/protocol/STTx.h>
|
||||||
#include <xrpl/protocol/Sign.h>
|
#include <xrpl/protocol/Sign.h>
|
||||||
#include <xrpl/protocol/TxFlags.h>
|
#include <xrpl/protocol/TxFlags.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
@@ -1318,19 +1320,31 @@ transactionSubmitMultiSigned(
|
|||||||
if (signers.empty())
|
if (signers.empty())
|
||||||
return RPC::make_param_error("tx_json.Signers array may not be empty.");
|
return RPC::make_param_error("tx_json.Signers array may not be empty.");
|
||||||
|
|
||||||
// The Signers array may only contain Signer objects.
|
// Recursively validate signer entry structure (including nested ones).
|
||||||
if (std::find_if_not(
|
// Feature enablement is enforced later in preflight; RPC only validates
|
||||||
signers.begin(), signers.end(), [](STObject const& obj) {
|
// shape here.
|
||||||
return (
|
std::function<bool(STArray const&, int)> validateSignersRecursive;
|
||||||
// A Signer object always contains these fields and no
|
validateSignersRecursive = [&](STArray const& arr, int depth) -> bool {
|
||||||
// others.
|
if (depth > nestedMultiSignMaxDepth)
|
||||||
obj.isFieldPresent(sfAccount) &&
|
return false;
|
||||||
obj.isFieldPresent(sfSigningPubKey) &&
|
|
||||||
obj.isFieldPresent(sfTxnSignature) && obj.getCount() == 3);
|
for (auto const& signer : arr)
|
||||||
}) != signers.end())
|
{
|
||||||
|
if (!isValidSignerEntry(signer))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (isNestedSigner(signer) &&
|
||||||
|
!validateSignersRecursive(
|
||||||
|
signer.getFieldArray(sfSigners), depth + 1))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!validateSignersRecursive(signers, 1))
|
||||||
{
|
{
|
||||||
return RPC::make_param_error(
|
return RPC::make_param_error(
|
||||||
"Signers array may only contain Signer entries.");
|
"Signers array may only contain valid Signer entries.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The array must be sorted and validated.
|
// The array must be sorted and validated.
|
||||||
|
|||||||
Reference in New Issue
Block a user