mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-20 10:35:50 +00:00
Compare commits
7 Commits
nd-migrate
...
multi-sig-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f8fff4672 | ||
|
|
a5b2904171 | ||
|
|
2b5906e73b | ||
|
|
96e7cc5299 | ||
|
|
64c707e21b | ||
|
|
b9cee56165 | ||
|
|
1d42b2ac41 |
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user