Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmake/CodeCoverage.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -468,4 +468,9 @@ function (add_code_coverage_to_target name scope)

target_link_libraries(${name} ${scope} $<$<LINK_LANGUAGE:CXX>:${COVERAGE_CXX_LINKER_FLAGS}>
$<$<LINK_LANGUAGE:C>:${COVERAGE_C_LINKER_FLAGS}>)
# GCC requires explicit libgcov linkage; Clang/AppleClang handles
# coverage via --coverage without a separate library.
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_link_libraries(${name} ${scope} gcov)
endif ()
endfunction () # add_code_coverage_to_target
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ words:
- Merkle
- Metafuncton
- misprediction
- msigner
- mptbalance
- MPTDEX
- mptflags
Expand Down
69 changes: 69 additions & 0 deletions include/xrpl/protocol/STTx.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,73 @@ STTx::getTransactionID() const
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 at maxMultiSigners per array).
*/
constexpr std::size_t nestedMultiSignMaxLeafSigners = 64;

//------------------------------------------------------------------------------
// Multi-sign signer entry helpers
//------------------------------------------------------------------------------

/** Count the number of present (non-STI_NOTPRESENT) fields in an STObject.
STObject::getCount() returns v_.size() which includes template slots for
optional fields that aren't set. This helper counts 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;
}

/** Check if a signer entry is a leaf signer (has signature fields).
A leaf signer has exactly 3 present fields:
Account + SigningPubKey + TxnSignature (no nested Signers).
*/
inline bool
isLeafSigner(STObject const& signer)
{
return signer.isFieldPresent(sfAccount) && signer.isFieldPresent(sfSigningPubKey) &&
signer.isFieldPresent(sfTxnSignature) && !signer.isFieldPresent(sfSigners) && countPresentFields(signer) == 3;
}

/** Check if a signer entry is a nested signer (delegates to sub-signers).
A nested signer has exactly 2 present fields:
Account + Signers (no SigningPubKey/TxnSignature).
*/
inline bool
isNestedSigner(STObject const& signer)
{
return signer.isFieldPresent(sfAccount) && signer.isFieldPresent(sfSigners) &&
!signer.isFieldPresent(sfSigningPubKey) && !signer.isFieldPresent(sfTxnSignature) &&
countPresentFields(signer) == 2;
}

/** Check if a signer entry has valid structure for multi-signing.
Returns true if the entry is either a valid leaf or nested signer.
*/
inline bool
isValidSignerEntry(STObject const& signer)
{
return isLeafSigner(signer) || isNestedSigner(signer);
}

} // namespace xrpl
1 change: 1 addition & 0 deletions include/xrpl/protocol/detail/features.macro
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ XRPL_FEATURE(Clawback, Supported::yes, VoteBehavior::DefaultNo
XRPL_FIX (UniversalNumber, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(XRPFees, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (RemoveNFTokenAutoTrustLine, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(NestedMultiSign, Supported::yes, VoteBehavior::DefaultNo)

// The following amendments are obsolete, but must remain supported
// because they could potentially get enabled.
Expand Down
5 changes: 3 additions & 2 deletions src/libxrpl/protocol/InnerObjectFormats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ InnerObjectFormats::InnerObjectFormats()
sfSigner.getCode(),
{
{sfAccount, soeREQUIRED},
{sfSigningPubKey, soeREQUIRED},
{sfTxnSignature, soeREQUIRED},
{sfSigningPubKey, soeOPTIONAL},
{sfTxnSignature, soeOPTIONAL},
{sfSigners, soeOPTIONAL},
});

add(sfMajority.jsonName,
Expand Down
133 changes: 91 additions & 42 deletions src/libxrpl/protocol/STTx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -431,58 +431,104 @@ multiSignHelper(

STArray const& signers{sigObject.getFieldArray(sfSigners)};

// There are well known bounds that the number of signers must be within.
if (signers.size() < STTx::minMultiSigners || signers.size() > STTx::maxMultiSigners)
return Unexpected("Invalid Signers array size.");
// Set max depth and leaf cap based on feature flag
bool const nested = rules.enabled(featureNestedMultiSign);
int const maxDepth = nested ? nestedMultiSignMaxDepth : legacyMultiSignMaxDepth;
std::size_t const maxLeafSigners = nested ? nestedMultiSignMaxLeafSigners : STTx::maxMultiSigners;
std::size_t totalLeafSigners = 0;

// Define recursive lambda for checking signatures at any depth
// parentAccountID identifies which account the signers are signing for
// (used for context in recursive calls, not currently validated against)
std::function<Expected<void, std::string>(STArray const&, AccountID const&, int)> checkSignersArray;

checkSignersArray = [&](STArray const& signersArray,
[[maybe_unused]] AccountID const& parentAccountID,
int depth) -> Expected<void, std::string> {
// Check depth limit
if (depth > maxDepth)
return Unexpected("Multi-signing depth limit exceeded.");

// There are well known bounds that the number of signers must be
// within.
if (signersArray.size() < STTx::minMultiSigners || signersArray.size() > STTx::maxMultiSigners)
return Unexpected("Invalid Signers array size.");

// Signers must be in sorted order by AccountID.
AccountID lastAccountID(beast::zero);

for (auto const& signer : signersArray)
{
auto const accountID = signer.getAccountID(sfAccount);

// Signers must be in sorted order by AccountID.
AccountID lastAccountID(beast::zero);
// The account owner may not multisign for themselves.
if (accountID == txnAccountID)
return Unexpected("Invalid multisigner.");

for (auto const& signer : signers)
{
auto const accountID = signer.getAccountID(sfAccount);
// No duplicate signers allowed.
if (lastAccountID == accountID)
return Unexpected("Duplicate Signers not allowed.");

// The account owner may not usually multisign for themselves.
// If they can, txnAccountID will be unseated, which is not equal to any
// value.
if (txnAccountID == accountID)
return Unexpected("Invalid multisigner.");
// Accounts must be in order by account ID. No duplicates allowed.
if (lastAccountID > accountID)
return Unexpected("Unsorted Signers array.");

// No duplicate signers allowed.
if (lastAccountID == accountID)
return Unexpected("Duplicate Signers not allowed.");
// The next signature must be greater than this one.
lastAccountID = accountID;

// Accounts must be in order by account ID. No duplicates allowed.
if (lastAccountID > accountID)
return Unexpected("Unsorted Signers array.");
// Check signer type using helpers
if (isNestedSigner(signer))
{
// This is a nested multi-signer (has Signers, no signature)
if (maxDepth == 1)
{
// Amendment is not enabled
return Unexpected("FeatureNestedMultiSign is disabled");
}

// The next signature must be greater than this one.
lastAccountID = accountID;
// Recursively check nested signers
STArray const& nestedSigners = signer.getFieldArray(sfSigners);
auto result = checkSignersArray(nestedSigners, accountID, depth + 1);
if (!result)
return result;
}
else if (isLeafSigner(signer))
{
// This is a leaf signer - verify the signature
if (++totalLeafSigners > maxLeafSigners)
return Unexpected(std::string("Too many leaf signers."));

// Verify the signature.
bool validSig = false;
std::optional<std::string> errorWhat;
try
{
auto spk = signer.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)))
// Verify the signature
bool validSig = false;
try
{
auto spk = signer.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)))
{
Blob const signature = signer.getFieldVL(sfTxnSignature);
validSig = verify(PublicKey(makeSlice(spk)), makeMsg(accountID).slice(), makeSlice(signature));
}
}
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
{
Blob const signature = signer.getFieldVL(sfTxnSignature);
validSig = verify(PublicKey(makeSlice(spk)), makeMsg(accountID).slice(), makeSlice(signature));
// Neither a valid leaf nor nested signer
return Unexpected(std::string("Malformed signer entry for account ") + toBase58(accountID) + ".");
}
}
catch (std::exception const& e)
{
// We assume any problem lies with the signature.
validSig = false;
errorWhat = e.what();
}
if (!validSig)
return Unexpected(
std::string("Invalid signature on account ") + toBase58(accountID) + errorWhat.value_or("") + ".");
}
// All signatures verified.
return {};

return {};
};

// Start the recursive check at depth 1
return checkSignersArray(signers, txnAccountID.value_or(AccountID{}), 1);
}

Expected<void, std::string>
Expand Down Expand Up @@ -511,6 +557,9 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
// the account owner may not multisign for themselves.
auto const txnAccountID = &sigObject != this ? std::nullopt : std::optional<AccountID>(getAccountID(sfAccount));

// Set max depth based on feature flag
int const maxDepth = rules.enabled(featureNestedMultiSign) ? 4 : 1;

// 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.
Expand Down
Loading
Loading