Compare commits

...

7 Commits

Author SHA1 Message Date
Richard Holland
9f8fff4672 debugging tests 2025-09-09 14:00:29 +10:00
Richard Holland
a5b2904171 more debugging 2025-08-31 16:45:59 +10:00
Richard Holland
2b5906e73b debugging 2025-08-31 15:25:12 +10:00
Richard Holland
96e7cc5299 ensure optional fields for nested signing 2025-08-20 13:46:45 +10:00
Richard Holland
64c707e21b add test cases compiling not tested 2025-08-20 13:37:02 +10:00
Richard Holland
b9cee56165 transactor nested multisig compiling not tested 2025-08-20 12:59:39 +10:00
Richard Holland
1d42b2ac41 inital commit nested multisign 2025-08-20 12:25:52 +10:00
11 changed files with 983 additions and 196 deletions

View File

@@ -82,6 +82,7 @@ preflight0(PreflightContext const& ctx)
{ {
JLOG(ctx.j.warn()) JLOG(ctx.j.warn())
<< "applyTransaction: transaction id may not be zero"; << "applyTransaction: transaction id may not be zero";
std::cout << "temINVALID " << __LINE__ << "\n";
return temINVALID; return temINVALID;
} }
@@ -130,7 +131,10 @@ preflight1(PreflightContext const& ctx)
{ {
if (ctx.tx.getSeqProxy().isTicket() && if (ctx.tx.getSeqProxy().isTicket() &&
ctx.tx.isFieldPresent(sfAccountTxnID)) ctx.tx.isFieldPresent(sfAccountTxnID))
{
std::cout << "temINVALID " << __LINE__ << "\n";
return temINVALID; return temINVALID;
}
return tesSUCCESS; return tesSUCCESS;
} }
@@ -163,7 +167,10 @@ preflight1(PreflightContext const& ctx)
// We return temINVALID for such transactions. // We return temINVALID for such transactions.
if (ctx.tx.getSeqProxy().isTicket() && if (ctx.tx.getSeqProxy().isTicket() &&
ctx.tx.isFieldPresent(sfAccountTxnID)) ctx.tx.isFieldPresent(sfAccountTxnID))
{
std::cout << "temINVALID " << __LINE__ << "\n";
return temINVALID; return temINVALID;
}
return tesSUCCESS; return tesSUCCESS;
} }
@@ -181,6 +188,7 @@ preflight2(PreflightContext const& ctx)
if (sigValid.first == Validity::SigBad) if (sigValid.first == Validity::SigBad)
{ {
JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second; JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second;
std::cout << "temINVALID " << __LINE__ << "\n";
return temINVALID; return temINVALID;
} }
return tesSUCCESS; return tesSUCCESS;
@@ -289,8 +297,40 @@ 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))
{
// Define recursive lambda to count all leaf signers
std::function<std::size_t(STArray const&)> countSigners;
countSigners = [&](STArray const& signers) -> std::size_t {
std::size_t count = 0;
for (auto const& signer : signers)
{
if (signer.isFieldPresent(sfSigners))
{
// This is a nested signer - recursively count its signers
count += countSigners(signer.getFieldArray(sfSigners));
}
else
{
// This is a leaf signer (one who actually signs)
// Count it only if it has signing fields (not just a
// placeholder)
if (signer.isFieldPresent(sfSigningPubKey) &&
signer.isFieldPresent(sfTxnSignature))
{
count += 1;
}
}
}
return count;
};
signerCount = countSigners(tx.getFieldArray(sfSigners));
}
XRPAmount hookExecutionFee{0}; XRPAmount hookExecutionFee{0};
uint64_t burden{1}; uint64_t burden{1};
@@ -923,157 +963,246 @@ 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 = // Set max depth based on feature flag
ctx.view.read(keylet::signers(id)); bool const allowNested = ctx.view.rules().enabled(featureNestedMultiSign);
// If the signer list doesn't exist the account is not multi-signing. int const maxDepth = allowNested ? 4 : 1;
if (!sleAccountSigners)
std::string lineno = "(unknown)";
if (ctx.tx.isFieldPresent(sfMemos))
{ {
JLOG(ctx.j.trace()) auto const& memos = ctx.tx.getFieldArray(sfMemos);
<< "applyTransaction: Invalid: Not a multi-signing account."; for (auto const& memo : memos)
return tefNOT_MULTI_SIGNING; {
auto memoObj = dynamic_cast<STObject const*>(&memo);
auto hex = memoObj->getFieldVL(sfMemoData);
lineno = strHex(hex);
break;
}
} }
// We have plans to support multiple SignerLists in the future. The // Define recursive lambda for checking signers at any depth
// presence and defaulted value of the SignerListID field will enable that. std::function<NotTEC(AccountID const&, STArray const&, int)>
assert(sleAccountSigners->isFieldPresent(sfSignerListID)); validateSigners;
assert(sleAccountSigners->getFieldU32(sfSignerListID) == 0);
auto accountSigners = validateSigners =
SignerEntries::deserialize(*sleAccountSigners, ctx.j, "ledger"); [&](AccountID const& acc, STArray const& signers, int depth) -> NotTEC {
if (!accountSigners) // Check depth limit
return accountSigners.error(); if (depth > maxDepth)
// Get the array of transaction signers.
STArray const& txSigners(ctx.tx.getFieldArray(sfSigners));
// 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 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()) if (allowNested)
{ {
JLOG(ctx.j.trace()) JLOG(ctx.j.trace())
<< "applyTransaction: Invalid SigningAccount.Account."; << "applyTransaction: Multi-signing depth limit exceeded.";
std::cout << "tefBAD_SIGNATURE: " << __LINE__ << "\n";
return tefBAD_SIGNATURE; return tefBAD_SIGNATURE;
} }
}
if (iter->account != txSignerAcctID) JLOG(ctx.j.warn())
{ << "applyTransaction: Nested multisigning disabled.";
// The SigningAccount is not in the SignerEntries.
JLOG(ctx.j.trace()) std::cout << "!!! temMALFORMED " << __FILE__ << " " << __LINE__
<< "applyTransaction: Invalid SigningAccount.Account."; << "\n";
return tefBAD_SIGNATURE; return temMALFORMED;
} }
// We found the SigningAccount in the list of valid signers. Now we // Get the SignerList for the account we're validating signers for
// need to compute the accountID that is associated with the signer's std::shared_ptr<STLedgerEntry const> sleAllowedSigners =
// public key. ctx.view.read(keylet::signers(acc));
auto const spk = txSigner.getFieldVL(sfSigningPubKey);
if (!publicKeyType(makeSlice(spk))) // If the signer list doesn't exist, this account is not set up for
// multi-signing
if (!sleAllowedSigners)
{ {
JLOG(ctx.j.trace()) JLOG(ctx.j.trace()) << "applyTransaction: Invalid: Account " << acc
<< "checkMultiSign: signing public key type is unknown"; << " not set up for multi-signing.";
return tefBAD_SIGNATURE; return tefNOT_MULTI_SIGNING;
} }
AccountID const signingAcctIDFromPubKey = uint32_t quorum = sleAllowedSigners->getFieldU32(sfSignerQuorum);
calcAccountID(PublicKey(makeSlice(spk))); uint32_t sum{0};
// Verify that the signingAcctID and the signingAcctIDFromPubKey auto allowedSigners =
// belong together. Here is are the rules: SignerEntries::deserialize(*sleAllowedSigners, ctx.j, "ledger");
// if (!allowedSigners)
// 1. "Phantom account": an account that is not in the ledger return allowedSigners.error();
// 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 std::set<AccountID> allowedSignerSet;
// the ledger. Determine that now. for (auto const& as : *allowedSigners)
auto sleTxSignerRoot = ctx.view.read(keylet::account(txSignerAcctID)); allowedSignerSet.emplace(as.account);
if (signingAcctIDFromPubKey == txSignerAcctID) // Walk the signers array, validating each signer
auto iter = allowedSigners->begin();
for (auto const& signerEntry : signers)
{ {
// Either Phantom or Master. Phantoms automatically pass. AccountID const signer = signerEntry.getAccountID(sfAccount);
if (sleTxSignerRoot) bool const isNested = signerEntry.isFieldPresent(sfSigners);
// Find this signer in the authorized SignerEntries list
while (iter->account < signer)
{ {
// Master Key. Account may not have asfDisableMaster set. std::cout << "iter acc: " << to_string(iter->account) << " < "
std::uint32_t const signerAccountFlags = << to_string(signer) << "\n";
sleTxSignerRoot->getFieldU32(sfFlags); if (++iter == allowedSigners->end())
if (signerAccountFlags & lsfDisableMaster)
{ {
JLOG(ctx.j.trace()) JLOG(ctx.j.trace())
<< "applyTransaction: Signer:Account lsfDisableMaster."; << "applyTransaction: Invalid SigningAccount.Account.";
return tefMASTER_DISABLED; std::cout << "tefBAD_SIGNATURE: " << __LINE__
<< " in signer set? "
<< (allowedSignerSet.find(signer) ==
allowedSignerSet.end()
? "n"
: "y")
<< "\n";
return tefBAD_SIGNATURE;
} }
} }
if (iter->account != signer)
{
// The SigningAccount is not in the SignerEntries.
JLOG(ctx.j.trace())
<< "applyTransaction: Invalid SigningAccount.Account.";
std::cout << "tefBAD_SIGNATURE: " << __LINE__ << "\n";
return tefBAD_SIGNATURE;
}
// Check if this signer has nested signers (delegation)
if (signerEntry.isFieldPresent(sfSigners))
{
// This is a nested multi-signer that delegates to sub-signers
if (signerEntry.isFieldPresent(sfSigningPubKey) ||
signerEntry.isFieldPresent(sfTxnSignature))
{
JLOG(ctx.j.trace())
<< "applyTransaction: Signer cannot have both nested "
"signers and signature fields.";
std::cout << "tefBAD_SIGNATURE: " << __LINE__ << "\n";
return tefBAD_SIGNATURE;
}
// Recursively validate the nested signers against
// signer's signer list
STArray const& nestedSigners =
signerEntry.getFieldArray(sfSigners);
NotTEC result =
validateSigners(signer, nestedSigners, depth + 1);
if (!isTesSuccess(result))
return result;
// If we get here, the nested signers met their quorum
// So we add THIS signer's weight (from current level's signer
// list)
sum += iter->weight;
}
else
{
// This is a leaf signer - validate signature as before
if (!signerEntry.isFieldPresent(sfSigningPubKey) ||
!signerEntry.isFieldPresent(sfTxnSignature))
{
JLOG(ctx.j.trace())
<< "applyApplication: Leaf signer must have "
"SigningPubKey and TxnSignature.";
std::cout << "tefBAD_SIGNATURE: " << __LINE__ << "\n";
return tefBAD_SIGNATURE;
}
auto const spk = signerEntry.getFieldVL(sfSigningPubKey);
if (!publicKeyType(makeSlice(spk)))
{
JLOG(ctx.j.trace())
<< "checkMultiSign: signing public key type is unknown";
std::cout << "tefBAD_SIGNATURE: " << __LINE__ << "\n";
return tefBAD_SIGNATURE;
}
AccountID const signingAcctIDFromPubKey =
calcAccountID(PublicKey(makeSlice(spk)));
auto sleTxSignerRoot = ctx.view.read(keylet::account(signer));
if (signingAcctIDFromPubKey == signer)
{
if (sleTxSignerRoot)
{
std::uint32_t const signerAccountFlags =
sleTxSignerRoot->getFieldU32(sfFlags);
if (signerAccountFlags & lsfDisableMaster)
{
JLOG(ctx.j.trace())
<< "applyTransaction: Signer:Account "
"lsfDisableMaster.";
return tefMASTER_DISABLED;
}
}
}
else
{
if (!sleTxSignerRoot)
{
JLOG(ctx.j.trace())
<< "applyTransaction: Non-phantom signer "
"lacks account root.";
std::cout << "tefBAD_SIGNATURE: " << __LINE__ << "\n";
return tefBAD_SIGNATURE;
}
if (!sleTxSignerRoot->isFieldPresent(sfRegularKey))
{
JLOG(ctx.j.trace())
<< "applyTransaction: Account lacks RegularKey.";
std::cout << "tefBAD_SIGNATURE: " << __LINE__ << "\n";
return tefBAD_SIGNATURE;
}
if (signingAcctIDFromPubKey !=
sleTxSignerRoot->getAccountID(sfRegularKey))
{
JLOG(ctx.j.trace()) << "applyTransaction: Account "
"doesn't match RegularKey.";
std::cout << "tefBAD_SIGNATURE: " << __LINE__ << "\n";
return tefBAD_SIGNATURE;
}
}
// Valid leaf signer - add their weight
sum += iter->weight;
}
char spacing[] = " ";
spacing[depth] = '\0';
std::cout << spacing << "sig check: "
<< "line: " << lineno << ", a=" << to_string(acc)
<< ", s=" << to_string(signer) << ", w=" << iter->weight
<< ", l=" << (isNested ? "f" : "t") << ", d=" << depth
<< ", " << sum << "/" << quorum << "\n";
} }
else
// Check if this level's accumulated weight meets its required quorum
if (sum < quorum)
{ {
// May be a Regular Key. Let's find out. JLOG(ctx.j.trace())
// Public key must hash to the account's regular key. << "applyTransaction: Signers failed to meet quorum at depth "
if (!sleTxSignerRoot) << depth;
{ return tefBAD_QUORUM;
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. return tesSUCCESS;
if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum)) };
STArray const& entries(ctx.tx.getFieldArray(sfSigners));
NotTEC result = validateSigners(id, entries, 1);
if (!isTesSuccess(result))
{ {
JLOG(ctx.j.trace()) std::cout << "Error: " << transToken(result) << "\n";
<< "applyTransaction: Signers failed to meet quorum."; return result;
return tefBAD_QUORUM;
} }
// Met the quorum. Continue. // The quorum check is already done inside validateSigners for the top level
// so if we get here, we've met the quorum
return tesSUCCESS; return tesSUCCESS;
} }

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how // Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this. // the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 85; static constexpr std::size_t numFeatures = 86;
/** Amendments that this server supports and the default voting behavior. /** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated Whether they are enabled depends on the Rules defined in the validated
@@ -373,6 +373,7 @@ extern uint256 const fixProvisionalDoubleThreading;
extern uint256 const featureClawback; extern uint256 const featureClawback;
extern uint256 const featureDeepFreeze; extern uint256 const featureDeepFreeze;
extern uint256 const featureIOUIssuerWeakTSH; extern uint256 const featureIOUIssuerWeakTSH;
extern uint256 const featureNestedMultiSign;
} // namespace ripple } // namespace ripple

View File

@@ -479,6 +479,7 @@ REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::De
REGISTER_FIX (fixProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(IOUIssuerWeakTSH, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(IOUIssuerWeakTSH, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(NestedMultiSign, Supported::yes, VoteBehavior::DefaultNo);
// The following amendments are obsolete, but must remain supported // The following amendments are obsolete, but must remain supported
// because they could potentially get enabled. // because they could potentially get enabled.

View File

@@ -44,8 +44,9 @@ InnerObjectFormats::InnerObjectFormats()
sfSigner.getCode(), sfSigner.getCode(),
{ {
{sfAccount, soeREQUIRED}, {sfAccount, soeREQUIRED},
{sfSigningPubKey, soeREQUIRED}, {sfSigningPubKey, soeOPTIONAL},
{sfTxnSignature, soeREQUIRED}, {sfTxnSignature, soeOPTIONAL},
{sfSigners, soeOPTIONAL},
}); });
add(sfMajority.jsonName.c_str(), add(sfMajority.jsonName.c_str(),

View File

@@ -369,64 +369,146 @@ 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 based on feature flag
{ int const maxDepth = rules.enabled(featureNestedMultiSign) ? 4 : 1;
auto const accountID = signer.getAccountID(sfAccount);
// 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) AccountID const& parentAccountID,
return Unexpected("Duplicate Signers not allowed."); int depth) -> Expected<void, std::string> {
// Check depth limit
// Accounts must be in order by account ID. No duplicates allowed. if (depth > maxDepth)
if (lastAccountID > accountID)
return Unexpected("Unsorted Signers array.");
// The next signature must be greater than this one.
lastAccountID = accountID;
// Verify the signature.
bool validSig = false;
try
{ {
Serializer s = dataStart; std::cout << "Multi-signing depth limit exceeded.\n";
finishMultiSigningData(accountID, s); return Unexpected("Multi-signing depth limit exceeded.");
}
auto spk = signer.getFieldVL(sfSigningPubKey); // There are well known bounds that the number of signers must be
// within.
if (signersArray.size() < minMultiSigners ||
signersArray.size() > maxMultiSigners(&rules))
{
std::cout << "Invalid Signers array size.\n";
return Unexpected("Invalid Signers array size.");
}
if (publicKeyType(makeSlice(spk))) // Signers must be in sorted order by AccountID.
AccountID lastAccountID(beast::zero);
for (auto const& signer : signersArray)
{
auto const accountID = signer.getAccountID(sfAccount);
// The account owner may not multisign for themselves.
if (accountID == txnAccountID)
{ {
Blob const signature = signer.getFieldVL(sfTxnSignature); std::cout << "Invalid multisigner.\n";
return Unexpected("Invalid multisigner.");
}
// wildcard network gets a free pass // No duplicate signers allowed.
validSig = isWildcardNetwork || if (lastAccountID == accountID)
verify(PublicKey(makeSlice(spk)), {
s.slice(), std::cout << "Duplicate Signers not allowed.\n";
makeSlice(signature), return Unexpected("Duplicate Signers not allowed.");
fullyCanonical); }
// Accounts must be in order by account ID. No duplicates allowed.
if (lastAccountID > accountID)
{
std::cout << "Unsorted Signers array.\n";
return Unexpected("Unsorted Signers array.");
}
// The next signature must be greater than this one.
lastAccountID = accountID;
// Check if this signer has nested signers
if (signer.isFieldPresent(sfSigners))
{
// This is a nested multi-signer
// Ensure it doesn't also have signature fields
if (signer.isFieldPresent(sfSigningPubKey) ||
signer.isFieldPresent(sfTxnSignature))
{
std::cout << "Signer cannot have both nested signers and "
"signature "
"fields.\n";
return Unexpected(
"Signer cannot have both nested signers and signature "
"fields.");
}
// Recursively check nested signers
STArray const& nestedSigners = signer.getFieldArray(sfSigners);
auto result =
checkSignersArray(nestedSigners, accountID, depth + 1);
if (!result)
return result;
}
else
{
// This is a leaf node - must have signature
if (!signer.isFieldPresent(sfSigningPubKey) ||
!signer.isFieldPresent(sfTxnSignature))
{
std::cout << "Leaf signer must have SigningPubKey and "
"TxnSignature.\n";
return Unexpected(
"Leaf signer must have SigningPubKey and "
"TxnSignature.");
}
// Verify the signature
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)
{
std::cout << std::string("Invalid signature on account ") +
toBase58(accountID) + ".\n";
return Unexpected(
std::string("Invalid signature on 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 {};
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------

View File

@@ -1183,12 +1183,32 @@ transactionSubmitMultiSigned(
// The Signers array may only contain Signer objects. // The Signers array may only contain Signer objects.
if (std::find_if_not( if (std::find_if_not(
signers.begin(), signers.end(), [](STObject const& obj) { signers.begin(), signers.end(), [](STObject const& obj) {
return ( if (obj.getCount() != 4 || !obj.isFieldPresent(sfAccount))
// A Signer object always contains these fields and no return false;
// others. // leaf signer
obj.isFieldPresent(sfAccount) && if (obj.isFieldPresent(sfSigningPubKey) &&
obj.isFieldPresent(sfSigningPubKey) && obj.isFieldPresent(sfTxnSignature) &&
obj.isFieldPresent(sfTxnSignature) && obj.getCount() == 3); !obj.isFieldPresent(sfSigners))
return true;
// nested signer
if (!obj.isFieldPresent(sfSigningPubKey) &&
!obj.isFieldPresent(sfTxnSignature) &&
obj.isFieldPresent(sfSigners))
return true;
/*
std::cout << "Error caused by:\n" <<
obj.getJson(JsonOptions::none) << "\n"
<< "obj.isFieldPresent(sfAccount) = " <<
(obj.isFieldPresent(sfAccount) ? "t" : "f") << "\n"
<< "obj.isFieldPresent(sfSigningPubKey) = " <<
(obj.isFieldPresent(sfSigningPubKey) ? "t" : "f") << "\n"
<< "obj.isFieldPresent(sfTxnSignature) = " <<
(obj.isFieldPresent(sfTxnSignature) ? "t" : "f") << "\n"
<< "obj.getCount() = " << obj.getCount() << "\n\n";
*/
return false;
}) != signers.end()) }) != signers.end())
{ {
return RPC::make_param_error( return RPC::make_param_error(

View File

@@ -1659,6 +1659,419 @@ public:
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
} }
void
test_nestedMultiSign(FeatureBitset features)
{
testcase("Nested MultiSign");
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define LINE_TO_HEX_STRING \
[]() -> std::string { \
const char* line = TOSTRING(__LINE__); \
int len = 0; \
while (line[len]) \
len++; \
std::string result; \
if (len % 2 == 1) \
{ \
result += (char)(0x00 * 16 + (line[0] - '0')); \
line++; \
} \
for (int i = 0; line[i]; i += 2) \
{ \
result += (char)((line[i] - '0') * 16 + (line[i + 1] - '0')); \
} \
return result; \
}()
#define M(m) memo(m, "", "")
#define L() memo(LINE_TO_HEX_STRING, "", "")
using namespace jtx;
Env env{*this, envconfig(), features};
Account const alice{"alice", KeyType::secp256k1};
Account const becky{"becky", KeyType::ed25519};
Account const cheri{"cheri", KeyType::secp256k1};
Account const daria{"daria", KeyType::ed25519};
Account const edgar{"edgar", KeyType::secp256k1};
Account const fiona{"fiona", KeyType::ed25519};
Account const grace{"grace", KeyType::secp256k1};
Account const henry{"henry", KeyType::ed25519};
Account const f1{"f1", KeyType::ed25519};
Account const f2{"f2", KeyType::ed25519};
Account const f3{"f3", KeyType::ed25519};
env.fund(
XRP(1000),
alice,
becky,
cheri,
daria,
edgar,
fiona,
grace,
henry,
f1,
f2,
f3,
phase,
jinni,
acc10,
acc11,
acc12);
env.close();
std::cout << "alice: " << to_string(alice) << "\n";
std::cout << "becky: " << to_string(becky) << "\n";
std::cout << "cheri: " << to_string(cheri) << "\n";
std::cout << "daria: " << to_string(daria) << "\n";
std::cout << "edgar: " << to_string(edgar) << "\n";
std::cout << "fiona: " << to_string(fiona) << "\n";
std::cout << "grace: " << to_string(grace) << "\n";
std::cout << "henry: " << to_string(henry) << "\n";
std::cout << "f1: " << to_string(f1) << "\n";
std::cout << "f2: " << to_string(f2) << "\n";
std::cout << "f3: " << to_string(f3) << "\n";
std::cout << "phase: " << to_string(phase) << "\n";
std::cout << "jinni: " << to_string(jinni) << "\n";
std::cout << "acc10: " << to_string(acc10) << "\n";
std::cout << "acc11: " << to_string(acc11) << "\n";
std::cout << "acc12: " << to_string(acc12) << "\n";
auto const baseFee = env.current()->fees().base;
if (!features[featureNestedMultiSign])
{
// When feature is disabled, nested signing should fail
env(signers(f1, 1, {{f2, 1}}));
env(signers(f2, 1, {{f3, 1}}));
env.close();
std::uint32_t f1Seq = env.seq(f1);
env(noop(f1),
msig({msigner(f2, msigner(f3))}),
L(),
fee(3 * baseFee),
ter(temINVALID));
env.close();
BEAST_EXPECT(env.seq(f1) == f1Seq);
return;
}
// Test Case 1: Basic 2-level nested signing with quorum
{
// Set up signer lists with quorum requirements
env(signers(becky, 2, {{bogie, 1}, {demon, 1}, {ghost, 1}}));
env(signers(cheri, 3, {{haunt, 2}, {jinni, 2}}));
env.close();
// Alice requires quorum of 3 with weighted signers
env(signers(alice, 3, {{becky, 2}, {cheri, 2}, {daria, 1}}));
env.close();
// Test 1a: becky alone (weight 2) doesn't meet alice's quorum
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(becky, msigner(bogie), msigner(demon))}),
L(),
fee(4 * baseFee),
ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Test 1b: becky (2) + daria (1) meets quorum of 3
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(becky, msigner(bogie), msigner(demon)),
msigner(daria)}),
L(),
fee(5 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Test 1c: cheri's nested signers must meet her quorum
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(
becky,
msigner(bogie),
msigner(demon)), // becky has a satisfied quorum
msigner(cheri, msigner(haunt))}), // but cheri does not
// (needs jinni too)
L(),
fee(5 * baseFee),
ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Test 1d: cheri with both signers meets her quorum
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(cheri, msigner(haunt), msigner(jinni)),
msigner(daria)}),
L(),
fee(5 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 2: 3-level maximum depth with quorum at each level
{
// Level 2: phase needs direct signatures (no deeper nesting)
env(signers(phase, 2, {{acc10, 1}, {acc11, 1}, {acc12, 1}}));
// Level 1: jinni needs weighted signatures
env(signers(jinni, 3, {{phase, 2}, {shade, 2}, {spook, 1}}));
// Level 0: edgar needs 2 from weighted signers
env(signers(edgar, 2, {{jinni, 1}, {bogie, 1}, {demon, 1}}));
// Alice now requires edgar with weight 3
env(signers(alice, 3, {{edgar, 3}, {fiona, 2}}));
env.close();
// Test 2a: 3-level signing with phase signing directly (not through
// nested signers)
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({
msigner(
edgar,
msigner(
jinni,
msigner(phase), // phase signs directly at level 3
msigner(shade)) // jinni quorum: 2+2 = 4 >= 3 ✓
) // edgar quorum: 1+0 = 1 < 2 ✗
}),
L(),
fee(4 * baseFee),
ter(tefBAD_QUORUM));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Test 2b: Edgar needs to meet his quorum too
aliceSeq = env.seq(alice);
env(noop(alice),
msig({
msigner(
edgar,
msigner(
jinni,
msigner(phase), // phase signs directly
msigner(shade)),
msigner(bogie)) // edgar quorum: 1+1 = 2 ✓
}),
L(),
fee(5 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Test 2c: Use phase's signers (making it effectively 3-level from
// alice)
aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(
edgar,
msigner(
jinni,
msigner(phase, msigner(acc10), msigner(acc11)),
msigner(spook)),
msigner(bogie))}),
L(),
fee(6 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 3: Mixed levels - some direct, some nested at different
// depths (max 3)
{
// Set up mixed-level signing for alice
// grace has direct signers
env(signers(grace, 2, {{bogie, 1}, {demon, 1}}));
// henry has 2-level signers (henry -> becky -> bogie/demon)
env(signers(henry, 1, {{becky, 1}, {cheri, 1}}));
// edgar can be signed for by bogie
env(signers(edgar, 1, {{bogie, 1}}));
// Alice has mix of direct and nested signers at different weights
env(signers(
alice,
5,
{
{daria, 1}, // direct signer
{edgar, 2}, // has 2-level signers
{fiona, 1}, // direct signer
{grace, 2}, // has direct signers
{henry, 2} // has 2-level signers
}));
env.close();
// Test 3a: Mix of all levels meeting quorum exactly
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({
msigner(daria), // weight 1, direct
msigner(edgar, msigner(bogie)), // weight 2, 2-level
msigner(grace, msigner(bogie), msigner(demon)) // weight 2,
// 2-level
}),
L(),
fee(6 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Test 3b: 3-level signing through henry
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(fiona), // weight 1, direct
msigner(
grace, msigner(bogie)), // weight 2, 2-level (partial)
msigner(
henry, // weight 2, 3-level
msigner(becky, msigner(bogie), msigner(demon)))}),
L(),
fee(6 * baseFee),
ter(tefBAD_QUORUM)); // grace didn't meet quorum
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
// Test 3c: Correct version with all quorums met
aliceSeq = env.seq(alice);
env(noop(alice),
msig({
msigner(
henry, // weight 2
msigner(becky, msigner(bogie), msigner(demon))),
msigner(fiona), // weight 1
msigner(edgar, msigner(bogie), msigner(demon)) // weight 2
}),
L(),
fee(8 * baseFee)); // Total weight: 1+2+2 = 5 ✓
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
// Test Case 4: Complex scenario with maximum signers at mixed depths
// (max 3)
{
// Create a signing tree that uses close to maximum signers
// and tests weight accumulation across all levels
// Set up for alice: needs 15 out of possible 20 weight
env(signers(
alice,
15,
{
{becky, 3}, // will use 2-level
{cheri, 3}, // will use 2-level
{daria, 3}, // will use direct
{edgar, 3}, // will use 2-level
{fiona, 3}, // will use direct
{grace, 3}, // will use direct
{henry, 2} // will use 2-level
}));
env.close();
// Complex multi-level transaction just meeting quorum
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({
msigner(
becky, // weight 3, 2-level
msigner(demon),
msigner(ghost)),
msigner(
cheri, // weight 3, 2-level
msigner(haunt),
msigner(jinni)),
msigner(daria), // weight 3, direct
msigner(
edgar, // weight 3, 2-level
msigner(bogie),
msigner(demon)),
msigner(grace) // weight 3, direct
}),
L(),
fee(10 * baseFee)); // Total weight: 3+3+3+3+3 = 15 ✓
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Test 4b: Test with henry using 3-level depth (maximum)
// First set up henry's chain properly
env(signers(henry, 1, {{jinni, 1}}));
env(signers(jinni, 2, {{acc10, 1}, {acc11, 1}}));
env.close();
aliceSeq = env.seq(alice);
env(noop(alice),
msig(
{msigner(
becky, // weight 3
msigner(demon)), // becky quorum not met!
msigner(
cheri, // weight 3
msigner(haunt),
msigner(jinni)),
msigner(daria), // weight 3
msigner(
henry, // weight 2, 3-level depth
msigner(jinni, msigner(acc10), msigner(acc11))),
msigner(
edgar, // weight 3
msigner(demon),
msigner(bogie))}),
L(),
fee(10 * baseFee),
ter(tefBAD_QUORUM)); // becky's quorum not met
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq);
}
// Test Case 5: Edge case - single signer with maximum nesting (depth 3)
{
// Alice needs just one signer, but that signer uses depth up to 3
env(signers(alice, 1, {{becky, 1}}));
env.close();
std::uint32_t aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(becky, msigner(demon), msigner(ghost))}),
L(),
fee(4 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
// Now with 3-level depth (maximum allowed)
// Structure: alice -> becky -> cheri -> jinni (jinni signs
// directly)
env(signers(becky, 1, {{cheri, 1}}));
env(signers(cheri, 1, {{jinni, 1}}));
// Note: We do NOT add signers to jinni to keep max depth at 3
env.close();
aliceSeq = env.seq(alice);
env(noop(alice),
msig({msigner(
becky,
msigner(
cheri,
msigner(jinni)))}), // jinni signs directly (depth 3)
L(),
fee(4 * baseFee));
env.close();
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
}
}
void void
testAll(FeatureBitset features) testAll(FeatureBitset features)
{ {
@@ -1680,6 +2093,7 @@ public:
test_signForHash(features); test_signForHash(features);
test_signersWithTickets(features); test_signersWithTickets(features);
test_signersWithTags(features); test_signersWithTags(features);
test_nestedMultiSign(features);
} }
void void
@@ -1692,8 +2106,11 @@ public:
// featureMultiSignReserve. Limits on the number of signers // featureMultiSignReserve. Limits on the number of signers
// changes based on featureExpandedSignerList. Test both with and // changes based on featureExpandedSignerList. Test both with and
// without. // without.
testAll(all - featureMultiSignReserve - featureExpandedSignerList); testAll(
testAll(all - featureExpandedSignerList); all - featureMultiSignReserve - featureExpandedSignerList -
featureNestedMultiSign);
testAll(all - featureExpandedSignerList - featureNestedMultiSign);
testAll(all - featureNestedMultiSign);
testAll(all); testAll(all);
test_amendmentTransition(); test_amendmentTransition();
} }

View File

@@ -310,6 +310,7 @@ Env::submit(JTx const& jt)
{ {
// Parsing failed or the JTx is // Parsing failed or the JTx is
// otherwise missing the stx field. // otherwise missing the stx field.
std::cout << "!!! temMALFORMED " << __FILE__ << " " << __LINE__ << "\n";
ter_ = temMALFORMED; ter_ = temMALFORMED;
didApply = false; didApply = false;
} }

View File

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

View File

@@ -21,6 +21,7 @@
#define RIPPLE_TEST_JTX_MULTISIGN_H_INCLUDED #define RIPPLE_TEST_JTX_MULTISIGN_H_INCLUDED
#include <cstdint> #include <cstdint>
#include <memory>
#include <optional> #include <optional>
#include <test/jtx/Account.h> #include <test/jtx/Account.h>
#include <test/jtx/amount.h> #include <test/jtx/amount.h>
@@ -65,6 +66,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_), 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;
@@ -73,16 +116,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)
{ {
@@ -93,13 +133,25 @@ 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:
// Direct constructor with SignerPtr vector
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>
explicit msig(AccountType&& a0, Accounts&&... aN) explicit msig(AccountType&& a0, Accounts&&... aN)
: msig{std::vector<Reg>{ : msig{std::vector<Reg>{
@@ -112,6 +164,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. */

View File

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