mirror of
https://github.com/Xahau/xahaud.git
synced 2026-04-25 21:47:47 +00:00
Compare commits
19 Commits
export
...
multi-sig-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9010e6b119 | ||
|
|
e50c37f550 | ||
|
|
6106c356bb | ||
|
|
cb1a0d154b | ||
|
|
5d811b622e | ||
|
|
49c22a4b7d | ||
|
|
9c240a07a4 | ||
|
|
057dc5a66b | ||
|
|
12e1afb694 | ||
|
|
c355ad9971 | ||
|
|
a8d7b2619e | ||
|
|
775fb3a8b2 | ||
|
|
9f8fff4672 | ||
|
|
a5b2904171 | ||
|
|
2b5906e73b | ||
|
|
96e7cc5299 | ||
|
|
64c707e21b | ||
|
|
b9cee56165 | ||
|
|
1d42b2ac41 |
15
.github/actions/xahau-ga-dependencies/action.yml
vendored
15
.github/actions/xahau-ga-dependencies/action.yml
vendored
@@ -134,10 +134,17 @@ runs:
|
||||
- name: Export custom recipes
|
||||
shell: bash
|
||||
run: |
|
||||
conan export external/snappy --version 1.1.10 --user xahaud --channel stable
|
||||
conan export external/soci --version 4.0.3 --user xahaud --channel stable
|
||||
conan export external/wasmedge --version 0.11.2 --user xahaud --channel stable
|
||||
|
||||
# Export snappy if not already exported
|
||||
conan list snappy/1.1.10@xahaud/stable 2>/dev/null | (grep -q "not found" && exit 1 || exit 0) || \
|
||||
conan export external/snappy --version 1.1.10 --user xahaud --channel stable
|
||||
|
||||
# Export soci if not already exported
|
||||
conan list soci/4.0.3@xahaud/stable 2>/dev/null | (grep -q "not found" && exit 1 || exit 0) || \
|
||||
conan export external/soci --version 4.0.3 --user xahaud --channel stable
|
||||
|
||||
# Export wasmedge if not already exported
|
||||
conan list wasmedge/0.11.2@xahaud/stable 2>/dev/null | (grep -q "not found" && exit 1 || exit 0) || \
|
||||
conan export external/wasmedge --version 0.11.2 --user xahaud --channel stable
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
env:
|
||||
|
||||
12
.github/workflows/xahau-ga-macos.yml
vendored
12
.github/workflows/xahau-ga-macos.yml
vendored
@@ -43,14 +43,22 @@ jobs:
|
||||
# To isolate environments for each Runner, instead of installing globally with brew,
|
||||
# use mise to isolate environments for each Runner directory.
|
||||
- name: Setup toolchain (mise)
|
||||
uses: jdx/mise-action@v2
|
||||
uses: jdx/mise-action@v3.6.1
|
||||
with:
|
||||
cache: false
|
||||
install: true
|
||||
mise_toml: |
|
||||
[tools]
|
||||
cmake = "3.23.1"
|
||||
python = "3.12"
|
||||
pipx = "latest"
|
||||
conan = "2"
|
||||
ninja = "latest"
|
||||
ccache = "latest"
|
||||
|
||||
- name: Install tools via mise
|
||||
run: |
|
||||
mise install
|
||||
mise use cmake@3.23.1 python@3.12 pipx@latest conan@2 ninja@latest ccache@latest
|
||||
mise reshim
|
||||
echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH"
|
||||
|
||||
|
||||
@@ -350,10 +350,7 @@ enum hook_return_code : int64_t {
|
||||
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
|
||||
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 5000 modified state
|
||||
// entires in the combined hook chains
|
||||
TOO_MANY_NAMESPACES = -45,
|
||||
EXPORT_FAILURE = -46,
|
||||
TOO_MANY_EXPORTED_TXN = -47,
|
||||
|
||||
TOO_MANY_NAMESPACES = -45
|
||||
};
|
||||
|
||||
enum ExitType : uint8_t {
|
||||
@@ -367,7 +364,6 @@ const uint16_t max_state_modifications = 256;
|
||||
const uint8_t max_slots = 255;
|
||||
const uint8_t max_nonce = 255;
|
||||
const uint8_t max_emit = 255;
|
||||
const uint8_t max_export = 4;
|
||||
const uint8_t max_params = 16;
|
||||
const double fee_base_multiplier = 1.1f;
|
||||
|
||||
@@ -473,13 +469,6 @@ static const APIWhitelist import_whitelist_1{
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static const APIWhitelist import_whitelist_2{
|
||||
// clang-format off
|
||||
HOOK_API_DEFINITION(I64, xport, (I32, I32)),
|
||||
HOOK_API_DEFINITION(I64, xport_reserve, (I32)),
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
#undef HOOK_API_DEFINITION
|
||||
#undef I32
|
||||
#undef I64
|
||||
|
||||
@@ -1034,12 +1034,6 @@ validateGuards(
|
||||
{
|
||||
// PASS, this is a version 1 api
|
||||
}
|
||||
else if (rulesVersion & 0x04U &&
|
||||
hook_api::import_whitelist_2.find(import_name) !=
|
||||
hook_api::import_whitelist_2.end())
|
||||
{
|
||||
// PASS, this is an export api
|
||||
}
|
||||
else
|
||||
{
|
||||
GUARDLOG(hook::log::IMPORT_ILLEGAL)
|
||||
|
||||
@@ -406,17 +406,6 @@ DECLARE_HOOK_FUNCTION(
|
||||
uint32_t slot_no_tx,
|
||||
uint32_t slot_no_meta);
|
||||
|
||||
DECLARE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
xport,
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len,
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len);
|
||||
DECLARE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
xport_reserve,
|
||||
uint32_t count);
|
||||
/*
|
||||
DECLARE_HOOK_FUNCTION(int64_t, str_find, uint32_t hread_ptr,
|
||||
uint32_t hread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t mode,
|
||||
@@ -496,8 +485,6 @@ struct HookResult
|
||||
|
||||
std::queue<std::shared_ptr<ripple::Transaction>>
|
||||
emittedTxn{}; // etx stored here until accept/rollback
|
||||
std::queue<std::shared_ptr<ripple::Transaction>>
|
||||
exportedTxn{};
|
||||
HookStateMap& stateMap;
|
||||
uint16_t changedStateCount = 0;
|
||||
std::map<
|
||||
@@ -554,7 +541,6 @@ struct HookContext
|
||||
uint16_t ledger_nonce_counter{0};
|
||||
int64_t expected_etxn_count{-1}; // make this a 64bit int so the uint32
|
||||
// from the hookapi cant overflow it
|
||||
int64_t expected_export_count{-1};
|
||||
std::map<ripple::uint256, bool> nonce_used{};
|
||||
uint32_t generation =
|
||||
0; // used for caching, only generated when txn_generation is called
|
||||
@@ -891,9 +877,6 @@ public:
|
||||
ADD_HOOK_FUNCTION(meta_slot, ctx);
|
||||
ADD_HOOK_FUNCTION(xpop_slot, ctx);
|
||||
|
||||
ADD_HOOK_FUNCTION(xport, ctx);
|
||||
ADD_HOOK_FUNCTION(xport_reserve, ctx);
|
||||
|
||||
/*
|
||||
ADD_HOOK_FUNCTION(str_find, ctx);
|
||||
ADD_HOOK_FUNCTION(str_replace, ctx);
|
||||
|
||||
@@ -79,7 +79,7 @@ main(int argc, char** argv)
|
||||
|
||||
close(fd);
|
||||
|
||||
auto result = validateGuards(hook, std::cout, "", 7);
|
||||
auto result = validateGuards(hook, std::cout, "", 3);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
|
||||
@@ -1971,8 +1971,6 @@ hook::finalizeHookResult(
|
||||
// directory) if we are allowed to
|
||||
std::vector<std::pair<uint256 /* txnid */, uint256 /* emit nonce */>>
|
||||
emission_txnid;
|
||||
std::vector<uint256 /* txnid */>
|
||||
exported_txnid;
|
||||
|
||||
if (doEmit)
|
||||
{
|
||||
@@ -2028,58 +2026,6 @@ hook::finalizeHookResult(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DBG_PRINTF("exported txn count: %d\n", hookResult.exportedTxn.size());
|
||||
for (; hookResult.exportedTxn.size() > 0; hookResult.exportedTxn.pop())
|
||||
{
|
||||
auto& tpTrans = hookResult.exportedTxn.front();
|
||||
auto& id = tpTrans->getID();
|
||||
JLOG(j.trace()) << "HookExport[" << HR_ACC() << "]: " << id;
|
||||
|
||||
// exported txns must be marked bad by the hash router to ensure under
|
||||
// no circumstances they will enter consensus on *this* chain.
|
||||
applyCtx.app.getHashRouter().setFlags(id, SF_BAD);
|
||||
|
||||
std::shared_ptr<const ripple::STTx> ptr =
|
||||
tpTrans->getSTransaction();
|
||||
|
||||
auto exportedId = keylet::exportedTxn(id);
|
||||
auto sleExported = applyCtx.view().peek(exportedId);
|
||||
|
||||
if (!sleExported)
|
||||
{
|
||||
exported_txnid.emplace_back(id);
|
||||
|
||||
sleExported = std::make_shared<SLE>(exportedId);
|
||||
|
||||
// RH TODO: add a new constructor to STObject to avoid this
|
||||
// serder thing
|
||||
ripple::Serializer s;
|
||||
ptr->add(s);
|
||||
SerialIter sit(s.slice());
|
||||
|
||||
sleExported->emplace_back(ripple::STObject(sit, sfExportedTxn));
|
||||
auto page = applyCtx.view().dirInsert(
|
||||
keylet::exportedDir(), exportedId, [&](SLE::ref sle) {
|
||||
(*sle)[sfFlags] = lsfEmittedDir;
|
||||
});
|
||||
|
||||
if (page)
|
||||
{
|
||||
(*sleExported)[sfOwnerNode] = *page;
|
||||
applyCtx.view().insert(sleExported);
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "HookError[" << HR_ACC() << "]: "
|
||||
<< "Export Directory full when trying to insert "
|
||||
<< id;
|
||||
return tecDIR_FULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool const fixV2 = applyCtx.view().rules().enabled(fixXahauV2);
|
||||
@@ -2106,12 +2052,6 @@ hook::finalizeHookResult(
|
||||
meta.setFieldU16(
|
||||
sfHookEmitCount,
|
||||
emission_txnid.size()); // this will never wrap, hard limit
|
||||
if (applyCtx.view().rules().enabled(featureExport))
|
||||
{
|
||||
meta.setFieldU16(
|
||||
sfHookExportCount,
|
||||
exported_txnid.size());
|
||||
}
|
||||
meta.setFieldU16(sfHookExecutionIndex, exec_index);
|
||||
meta.setFieldU16(sfHookStateChangeCount, hookResult.changedStateCount);
|
||||
meta.setFieldH256(sfHookHash, hookResult.hookHash);
|
||||
@@ -3948,27 +3888,6 @@ DEFINE_HOOK_FUNCTION(int64_t, etxn_reserve, uint32_t count)
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
DEFINE_HOOK_FUNCTION(int64_t, xport_reserve, uint32_t count)
|
||||
{
|
||||
HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx,
|
||||
// hookCtx on current stack
|
||||
|
||||
if (hookCtx.expected_export_count > -1)
|
||||
return ALREADY_SET;
|
||||
|
||||
if (count < 1)
|
||||
return TOO_SMALL;
|
||||
|
||||
if (count > hook_api::max_export)
|
||||
return TOO_BIG;
|
||||
|
||||
hookCtx.expected_export_count = count;
|
||||
|
||||
return count;
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
// Compute the burden of an emitted transaction based on a number of factors
|
||||
DEFINE_HOOK_FUNCNARG(int64_t, etxn_burden)
|
||||
{
|
||||
@@ -6237,92 +6156,6 @@ DEFINE_HOOK_FUNCTION(
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
xport,
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len,
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
if (write_len < 32)
|
||||
return TOO_SMALL;
|
||||
|
||||
auto& app = hookCtx.applyCtx.app;
|
||||
|
||||
if (hookCtx.expected_export_count < 0)
|
||||
return PREREQUISITE_NOT_MET;
|
||||
|
||||
if (hookCtx.result.exportedTxn.size() >= hookCtx.expected_export_count)
|
||||
return TOO_MANY_EXPORTED_TXN;
|
||||
|
||||
ripple::Blob blob{memory + read_ptr, memory + read_ptr + read_len};
|
||||
|
||||
std::shared_ptr<STTx const> stpTrans;
|
||||
try
|
||||
{
|
||||
stpTrans = std::make_shared<STTx const>(
|
||||
SerialIter{memory + read_ptr, read_len});
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
JLOG(j.trace()) << "HookExport[" << HC_ACC() << "]: Failed " << e.what()
|
||||
<< "\n";
|
||||
return EXPORT_FAILURE;
|
||||
}
|
||||
|
||||
if (!stpTrans->isFieldPresent(sfAccount) ||
|
||||
stpTrans->getAccountID(sfAccount) != hookCtx.result.account)
|
||||
{
|
||||
JLOG(j.trace()) << "HookExport[" << HC_ACC()
|
||||
<< "]: Attempted to export a txn that's not for this Hook's Account ID.";
|
||||
return EXPORT_FAILURE;
|
||||
}
|
||||
|
||||
std::string reason;
|
||||
auto tpTrans = std::make_shared<Transaction>(stpTrans, reason, app);
|
||||
// RHTODO: is this needed or wise? VVV
|
||||
if (tpTrans->getStatus() != NEW)
|
||||
{
|
||||
JLOG(j.trace()) << "HookExport[" << HC_ACC()
|
||||
<< "]: tpTrans->getStatus() != NEW";
|
||||
return EXPORT_FAILURE;
|
||||
}
|
||||
auto const& txID = tpTrans->getID();
|
||||
|
||||
if (txID.size() > write_len)
|
||||
return TOO_SMALL;
|
||||
|
||||
if (NOT_IN_BOUNDS(write_ptr, txID.size(), memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
auto const write_txid = [&]() -> int64_t {
|
||||
WRITE_WASM_MEMORY_AND_RETURN(
|
||||
write_ptr,
|
||||
txID.size(),
|
||||
txID.data(),
|
||||
txID.size(),
|
||||
memory,
|
||||
memory_length);
|
||||
};
|
||||
|
||||
int64_t result = write_txid();
|
||||
|
||||
if (result == 32)
|
||||
hookCtx.result.exportedTxn.push(tpTrans);
|
||||
|
||||
return result;
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
/*
|
||||
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
|
||||
@@ -599,13 +599,6 @@ public:
|
||||
return validatorKeys_.publicKey;
|
||||
}
|
||||
|
||||
ValidatorKeys const&
|
||||
getValidatorKeys() const override
|
||||
{
|
||||
return validatorKeys_;
|
||||
}
|
||||
|
||||
|
||||
NetworkOPs&
|
||||
getOPs() override
|
||||
{
|
||||
|
||||
@@ -240,8 +240,7 @@ public:
|
||||
|
||||
virtual PublicKey const&
|
||||
getValidationPublicKey() const = 0;
|
||||
virtual ValidatorKeys const&
|
||||
getValidatorKeys() const = 0;
|
||||
|
||||
virtual Resource::Manager&
|
||||
getResourceManager() = 0;
|
||||
virtual PathRequests&
|
||||
|
||||
@@ -471,6 +471,10 @@ ManifestCache::applyManifest(Manifest m)
|
||||
|
||||
auto masterKey = m.masterKey;
|
||||
map_.emplace(std::move(masterKey), std::move(m));
|
||||
|
||||
// Increment sequence to invalidate cached manifest messages
|
||||
seq_++;
|
||||
|
||||
return ManifestDisposition::accepted;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
#include <ripple/app/misc/ValidatorKeys.h>
|
||||
#include <ripple/protocol/Sign.h>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
@@ -1541,247 +1539,6 @@ TxQ::accept(Application& app, OpenView& view)
|
||||
}
|
||||
}
|
||||
|
||||
// Inject exported transactions/signatures, if any
|
||||
if (view.rules().enabled(featureExport))
|
||||
{
|
||||
do
|
||||
{
|
||||
// if we're not a validator we do nothing here
|
||||
if (app.getValidationPublicKey().empty())
|
||||
break;
|
||||
|
||||
auto const& keys = app.getValidatorKeys();
|
||||
|
||||
if (keys.configInvalid())
|
||||
break;
|
||||
|
||||
// and if we're not on the UNLReport we also do nothing
|
||||
|
||||
auto const unlRep = view.read(keylet::UNLReport());
|
||||
if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators))
|
||||
{
|
||||
// nothing to do without a unlreport object
|
||||
break;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
auto const& avs = unlRep->getFieldArray(sfActiveValidators);
|
||||
for (auto const& av : avs)
|
||||
{
|
||||
if (PublicKey(av[sfPublicKey]) == keys.masterPublicKey)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
break;
|
||||
|
||||
// execution to here means we're a validator and on the UNLReport
|
||||
|
||||
AccountID signingAcc = calcAccountID(keys.publicKey);
|
||||
|
||||
Keylet const exportedDirKeylet{keylet::exportedDir()};
|
||||
if (dirIsEmpty(view, exportedDirKeylet))
|
||||
break;
|
||||
|
||||
std::shared_ptr<SLE const> sleDirNode{};
|
||||
unsigned int uDirEntry{0};
|
||||
uint256 dirEntry{beast::zero};
|
||||
|
||||
if (!cdirFirst(
|
||||
view,
|
||||
exportedDirKeylet.key,
|
||||
sleDirNode,
|
||||
uDirEntry,
|
||||
dirEntry))
|
||||
break;
|
||||
|
||||
do
|
||||
{
|
||||
Keylet const itemKeylet{ltCHILD, dirEntry};
|
||||
auto sleItem = view.read(itemKeylet);
|
||||
if (!sleItem)
|
||||
{
|
||||
// Directory node has an invalid index. Bail out.
|
||||
JLOG(j_.warn())
|
||||
<< "ExportedTxn processing: directory node in ledger "
|
||||
<< view.seq()
|
||||
<< " has index to object that is missing: "
|
||||
<< to_string(dirEntry);
|
||||
|
||||
// RH TODO: if this ever happens the entry should be
|
||||
// gracefully removed (somehow)
|
||||
continue;
|
||||
}
|
||||
|
||||
LedgerEntryType const nodeType{
|
||||
safe_cast<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
|
||||
|
||||
if (nodeType != ltEXPORTED_TXN)
|
||||
{
|
||||
JLOG(j_.warn())
|
||||
<< "ExportedTxn processing: emitted directory contained "
|
||||
"non ltEMITTED_TXN type";
|
||||
// RH TODO: if this ever happens the entry should be
|
||||
// gracefully removed (somehow)
|
||||
continue;
|
||||
}
|
||||
|
||||
JLOG(j_.info()) << "Processing exported txn: " << *sleItem;
|
||||
|
||||
auto const& exported =
|
||||
const_cast<ripple::STLedgerEntry&>(*sleItem)
|
||||
.getField(sfExportedTxn)
|
||||
.downcast<STObject>();
|
||||
|
||||
auto const& txnHash = sleItem->getFieldH256(sfTransactionHash);
|
||||
|
||||
auto exportedLgrSeq = exported.getFieldU32(sfLedgerSequence);
|
||||
|
||||
auto const seq = view.seq();
|
||||
|
||||
if (exportedLgrSeq == seq)
|
||||
{
|
||||
// this shouldn't happen, but do nothing
|
||||
continue;
|
||||
}
|
||||
|
||||
if (exportedLgrSeq < seq - 1)
|
||||
{
|
||||
// all old entries need to be turned into Export transactions so they can be removed
|
||||
// from the directory
|
||||
|
||||
// in the previous ledger all the ExportSign transactions were executed, and one-by-one
|
||||
// added the validators' signatures to the ltEXPORTED_TXN's sfSigners array.
|
||||
// now we need to collect these together and place them inside the ExportedTxn blob
|
||||
// and publish the blob in the Export transaction type.
|
||||
|
||||
STArray signers = sleItem->getFieldArray(sfSigners);
|
||||
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
exported.add(*s);
|
||||
SerialIter sitTrans(s->slice());
|
||||
try
|
||||
{
|
||||
auto stpTrans =
|
||||
std::make_shared<STTx>(std::ref(sitTrans));
|
||||
|
||||
if (!stpTrans->isFieldPresent(sfAccount) ||
|
||||
stpTrans->getAccountID(sfAccount) == beast::zero)
|
||||
{
|
||||
JLOG(j_.warn()) << "Hook: Export failure: "
|
||||
<< "sfAccount missing or zero.";
|
||||
// RH TODO: if this ever happens the entry should be
|
||||
// gracefully removed (somehow)
|
||||
continue;
|
||||
}
|
||||
|
||||
// RH TODO: should we force remove signingpubkey here?
|
||||
|
||||
stpTrans->setFieldArray(sfSigners, signers);
|
||||
|
||||
Blob const& blob = stpTrans->getSerializer().peekData();
|
||||
|
||||
STTx exportTx(ttEXPORT, [&](auto& obj) {
|
||||
obj.setFieldVL(sfExportedTxn, blob);
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
obj.setFieldH256(sfTransactionHash, txnHash);
|
||||
obj.setFieldArray(sfSigners, signers);
|
||||
});
|
||||
|
||||
// submit to the ledger
|
||||
{
|
||||
uint256 txID = exportTx.getTransactionID();
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
exportTx.add(*s);
|
||||
app.getHashRouter().setFlags(txID, SF_PRIVATE2);
|
||||
app.getHashRouter().setFlags(txID, SF_EMITTED);
|
||||
view.rawTxInsert(txID, std::move(s), nullptr);
|
||||
ledgerChanged = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
catch (std::exception& e)
|
||||
{
|
||||
JLOG(j_.warn())
|
||||
<< "ExportedTxn Processing: Failure: " << e.what()
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// this ledger is the one after the exported txn was added to the directory
|
||||
// so generate the export sign txns
|
||||
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
exported.add(*s);
|
||||
SerialIter sitTrans(s->slice());
|
||||
try
|
||||
{
|
||||
auto const& stpTrans =
|
||||
std::make_shared<STTx const>(std::ref(sitTrans));
|
||||
|
||||
if (!stpTrans->isFieldPresent(sfAccount) ||
|
||||
stpTrans->getAccountID(sfAccount) == beast::zero)
|
||||
{
|
||||
JLOG(j_.warn()) << "Hook: Export failure: "
|
||||
<< "sfAccount missing or zero.";
|
||||
// RH TODO: if this ever happens the entry should be
|
||||
// gracefully removed (somehow)
|
||||
continue;
|
||||
}
|
||||
|
||||
auto seq = view.info().seq;
|
||||
auto txnHash = stpTrans->getTransactionID();
|
||||
|
||||
Serializer s =
|
||||
buildMultiSigningData(*stpTrans, signingAcc);
|
||||
|
||||
auto multisig = ripple::sign(keys.publicKey, keys.secretKey, s.slice());
|
||||
|
||||
STTx exportSignTx(ttEXPORT_SIGN, [&](auto& obj) {
|
||||
obj.set(([&]() {
|
||||
auto inner = std::make_unique<STObject>(sfSigner);
|
||||
inner->setFieldVL(sfSigningPubKey, keys.publicKey);
|
||||
inner->setAccountID(sfAccount, signingAcc);
|
||||
inner->setFieldVL(sfTxnSignature, multisig);
|
||||
return inner;
|
||||
})());
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
obj.setFieldH256(sfTransactionHash, txnHash);
|
||||
});
|
||||
|
||||
// submit to the ledger
|
||||
{
|
||||
uint256 txID = exportSignTx.getTransactionID();
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
exportSignTx.add(*s);
|
||||
app.getHashRouter().setFlags(txID, SF_PRIVATE2);
|
||||
app.getHashRouter().setFlags(txID, SF_EMITTED);
|
||||
view.rawTxInsert(txID, std::move(s), nullptr);
|
||||
ledgerChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
catch (std::exception& e)
|
||||
{
|
||||
JLOG(j_.warn())
|
||||
<< "ExportedTxn Processing: Failure: " << e.what()
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
} while (cdirNext(
|
||||
view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry));
|
||||
|
||||
} while (0);
|
||||
|
||||
}
|
||||
|
||||
// Inject emitted transactions if any
|
||||
if (view.rules().enabled(featureHooks))
|
||||
do
|
||||
|
||||
@@ -96,13 +96,6 @@ Change::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if ((ctx.tx.getTxnType() == ttEXPORT_SIGN || ctx.tx.getTxnType() == ttEXPORT) &&
|
||||
!ctx.rules.enabled(featureExport))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Change: Export not enabled";
|
||||
return temDISABLED;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -161,8 +154,6 @@ Change::preclaim(PreclaimContext const& ctx)
|
||||
case ttAMENDMENT:
|
||||
case ttUNL_MODIFY:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttEXPORT:
|
||||
case ttEXPORT_SIGN:
|
||||
return tesSUCCESS;
|
||||
case ttUNL_REPORT: {
|
||||
if (!ctx.tx.isFieldPresent(sfImportVLKey) ||
|
||||
@@ -218,11 +209,6 @@ Change::doApply()
|
||||
return applyEmitFailure();
|
||||
case ttUNL_REPORT:
|
||||
return applyUNLReport();
|
||||
case ttEXPORT:
|
||||
return applyExport();
|
||||
case ttEXPORT_SIGN:
|
||||
return applyExportSign();
|
||||
|
||||
default:
|
||||
assert(0);
|
||||
return tefFAILURE;
|
||||
@@ -620,8 +606,7 @@ Change::activateXahauGenesis()
|
||||
loggerStream,
|
||||
"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
(ctx_.view().rules().enabled(featureHooksUpdate1) ? 1 : 0) +
|
||||
(ctx_.view().rules().enabled(fix20250131) ? 2 : 0) +
|
||||
(ctx_.view().rules().enabled(featureExport) ? 4 : 0));
|
||||
(ctx_.view().rules().enabled(fix20250131) ? 2 : 0));
|
||||
|
||||
if (!result)
|
||||
{
|
||||
@@ -1087,80 +1072,6 @@ Change::applyEmitFailure()
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Change::applyExport()
|
||||
{
|
||||
uint256 txnID(ctx_.tx.getFieldH256(sfTransactionHash));
|
||||
do
|
||||
{
|
||||
JLOG(j_.info()) << "HookExport[" << txnID
|
||||
<< "]: ttExport exporting transaction";
|
||||
|
||||
auto key = keylet::exportedTxn(txnID);
|
||||
|
||||
auto const& sle = view().peek(key);
|
||||
|
||||
if (!sle)
|
||||
{
|
||||
// most likely explanation is that this was somehow a double-up, so just ignore
|
||||
JLOG(j_.warn())
|
||||
<< "HookError[" << txnID << "]: ttExport could not find exported txn in ledger";
|
||||
break;
|
||||
}
|
||||
|
||||
if (!view().dirRemove(
|
||||
keylet::exportedDir(),
|
||||
sle->getFieldU64(sfOwnerNode),
|
||||
key,
|
||||
false))
|
||||
{
|
||||
JLOG(j_.fatal()) << "HookError[" << txnID
|
||||
<< "]: ttExport (Change) tefBAD_LEDGER";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
view().erase(sle);
|
||||
} while (0);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Change::applyExportSign()
|
||||
{
|
||||
uint256 txnID(ctx_.tx.getFieldH256(sfTransactionHash));
|
||||
do
|
||||
{
|
||||
JLOG(j_.info()) << "HookExport[" << txnID
|
||||
<< "]: ttExportSign adding signature to transaction";
|
||||
|
||||
auto key = keylet::exportedTxn(txnID);
|
||||
|
||||
auto const& sle = view().peek(key);
|
||||
|
||||
if (!sle)
|
||||
{
|
||||
// most likely explanation is that this was somehow a double-up, so just ignore
|
||||
JLOG(j_.warn())
|
||||
<< "HookError[" << txnID << "]: ttExportSign could not find exported txn in ledger";
|
||||
break;
|
||||
}
|
||||
|
||||
// grab the signer object off the txn
|
||||
STObject signerObj = const_cast<ripple::STTx&>(ctx_.tx)
|
||||
.getField(sfSigner)
|
||||
.downcast<STObject>();
|
||||
|
||||
// append it to the signers field in the ledger object
|
||||
STArray signers = sle->getFieldArray(sfSigners);
|
||||
signers.push_back(signerObj);
|
||||
sle->setFieldArray(sfSigners, signers);
|
||||
|
||||
// done
|
||||
view().update(sle);
|
||||
} while (0);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Change::applyUNLModify()
|
||||
{
|
||||
|
||||
@@ -74,12 +74,6 @@ private:
|
||||
TER
|
||||
applyEmitFailure();
|
||||
|
||||
TER
|
||||
applyExport();
|
||||
|
||||
TER
|
||||
applyExportSign();
|
||||
|
||||
TER
|
||||
applyUNLReport();
|
||||
};
|
||||
|
||||
@@ -37,12 +37,9 @@
|
||||
#include <charconv>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <ripple/app/hook/applyHook.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
static const uint256 shadowTicketNamespace = uint256::fromVoid("RESERVED NAMESPACE SHADOW TICKET");
|
||||
|
||||
TxConsequences
|
||||
Import::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -200,7 +197,7 @@ Import::preflight(PreflightContext const& ctx)
|
||||
if (!stpTrans || !meta)
|
||||
return temMALFORMED;
|
||||
|
||||
if (stpTrans->isFieldPresent(sfTicketSequence) && !ctx.rules.enabled(featureExport))
|
||||
if (stpTrans->isFieldPresent(sfTicketSequence))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Import: cannot use TicketSequence XPOP.";
|
||||
return temMALFORMED;
|
||||
@@ -891,26 +888,6 @@ Import::preclaim(PreclaimContext const& ctx)
|
||||
return tefINTERNAL;
|
||||
}
|
||||
|
||||
bool const hasTicket = stpTrans->isFieldPresent(sfTicketSequence);
|
||||
|
||||
if (hasTicket)
|
||||
{
|
||||
if (!ctx.view.rules().enabled(featureExport))
|
||||
return tefINTERNAL;
|
||||
|
||||
auto const acc = stpTrans->getAccountID(sfAccount);
|
||||
uint256 const seq = uint256(stpTrans->getFieldU32(sfTicketSequence));
|
||||
|
||||
// check if there is a shadow ticket, and if not we won't allow
|
||||
// the txn to pass into consensus
|
||||
|
||||
if (!ctx.view.exists(keylet::hookState(acc, seq, shadowTicketNamespace)))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Import: attempted to import a txn without shadow ticket.";
|
||||
return telSHADOW_TICKET_REQUIRED; // tel code to avoid consensus/forward without SF_BAD
|
||||
}
|
||||
}
|
||||
|
||||
auto const& sle = ctx.view.read(keylet::account(ctx.tx[sfAccount]));
|
||||
|
||||
auto const tt = stpTrans->getTxnType();
|
||||
@@ -951,17 +928,13 @@ Import::preclaim(PreclaimContext const& ctx)
|
||||
} while (0);
|
||||
}
|
||||
|
||||
if (!hasTicket)
|
||||
if (sle && sle->isFieldPresent(sfImportSequence))
|
||||
{
|
||||
uint32_t sleImportSequence = sle->getFieldU32(sfImportSequence);
|
||||
|
||||
if (sle && sle->isFieldPresent(sfImportSequence))
|
||||
{
|
||||
uint32_t sleImportSequence = sle->getFieldU32(sfImportSequence);
|
||||
|
||||
// replay attempt
|
||||
if (sleImportSequence >= stpTrans->getFieldU32(sfSequence))
|
||||
return tefPAST_IMPORT_SEQ;
|
||||
}
|
||||
// replay attempt
|
||||
if (sleImportSequence >= stpTrans->getFieldU32(sfSequence))
|
||||
return tefPAST_IMPORT_SEQ;
|
||||
}
|
||||
|
||||
// when importing for the first time the fee must be zero
|
||||
@@ -1269,11 +1242,7 @@ Import::doApply()
|
||||
auto const id = ctx_.tx[sfAccount];
|
||||
auto sle = view().peek(keylet::account(id));
|
||||
|
||||
std::optional<uint256> ticket;
|
||||
if (stpTrans->isFieldPresent(sfTicketSequence))
|
||||
ticket = uint256(stpTrans->getFieldU32(sfTicketSequence));
|
||||
|
||||
if (sle && !ticket.has_value() && sle->getFieldU32(sfImportSequence) >= importSequence)
|
||||
if (sle && sle->getFieldU32(sfImportSequence) >= importSequence)
|
||||
{
|
||||
// make double sure import seq hasn't passed
|
||||
JLOG(ctx_.journal.warn()) << "Import: ImportSequence passed";
|
||||
@@ -1366,24 +1335,8 @@ Import::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
if (!ticket.has_value())
|
||||
sle->setFieldU32(sfImportSequence, importSequence);
|
||||
|
||||
sle->setFieldU32(sfImportSequence, importSequence);
|
||||
sle->setFieldAmount(sfBalance, finalBal);
|
||||
|
||||
if (ticket.has_value())
|
||||
{
|
||||
auto sleTicket = view().peek(keylet::hookState(id, *ticket, shadowTicketNamespace));
|
||||
if (!sleTicket)
|
||||
return tefINTERNAL;
|
||||
|
||||
TER result = hook::setHookState(ctx_, id, shadowTicketNamespace, *ticket, {});
|
||||
if (result != tesSUCCESS)
|
||||
return result;
|
||||
|
||||
// RHUPTO: ticketseq billing?
|
||||
}
|
||||
|
||||
|
||||
if (create)
|
||||
{
|
||||
|
||||
@@ -491,8 +491,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
||||
logger,
|
||||
hsacc,
|
||||
(ctx.rules.enabled(featureHooksUpdate1) ? 1 : 0) +
|
||||
(ctx.rules.enabled(fix20250131) ? 2 : 0) +
|
||||
(ctx.rules.enabled(featureExport) ? 4 : 0));
|
||||
(ctx.rules.enabled(fix20250131) ? 2 : 0));
|
||||
|
||||
if (ctx.j.trace())
|
||||
{
|
||||
|
||||
@@ -289,8 +289,40 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
|
||||
// Each signer adds one more baseFee to the minimum required fee
|
||||
// for the transaction.
|
||||
std::size_t const signerCount =
|
||||
tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 0;
|
||||
std::size_t signerCount = 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};
|
||||
uint64_t burden{1};
|
||||
@@ -923,157 +955,282 @@ NotTEC
|
||||
Transactor::checkMultiSign(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const id = ctx.tx.getAccountID(sfAccount);
|
||||
// Get mTxnAccountID's SignerList and Quorum.
|
||||
std::shared_ptr<STLedgerEntry const> sleAccountSigners =
|
||||
ctx.view.read(keylet::signers(id));
|
||||
// If the signer list doesn't exist the account is not multi-signing.
|
||||
if (!sleAccountSigners)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "applyTransaction: Invalid: Not a multi-signing account.";
|
||||
return tefNOT_MULTI_SIGNING;
|
||||
}
|
||||
|
||||
// We have plans to support multiple SignerLists in the future. The
|
||||
// presence and defaulted value of the SignerListID field will enable that.
|
||||
assert(sleAccountSigners->isFieldPresent(sfSignerListID));
|
||||
assert(sleAccountSigners->getFieldU32(sfSignerListID) == 0);
|
||||
// Set max depth based on feature flag
|
||||
bool const allowNested = ctx.view.rules().enabled(featureNestedMultiSign);
|
||||
int const maxDepth = allowNested ? 4 : 1;
|
||||
|
||||
auto accountSigners =
|
||||
SignerEntries::deserialize(*sleAccountSigners, ctx.j, "ledger");
|
||||
if (!accountSigners)
|
||||
return accountSigners.error();
|
||||
// Define recursive lambda for checking signers at any depth
|
||||
// ancestors tracks the signing chain to detect cycles
|
||||
std::function<NotTEC(
|
||||
AccountID const&, STArray const&, int, std::set<AccountID>)>
|
||||
validateSigners;
|
||||
|
||||
// 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)
|
||||
validateSigners = [&](AccountID const& acc,
|
||||
STArray const& signers,
|
||||
int depth,
|
||||
std::set<AccountID> ancestors) -> NotTEC {
|
||||
// Cycle detection: if we're already validating this account up the
|
||||
// chain it cannot contribute - but this isn't an error, just
|
||||
// unavailable weight
|
||||
if (ancestors.count(acc))
|
||||
{
|
||||
if (++iter == accountSigners->end())
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Cyclic signer detected: " << acc;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Check depth limit
|
||||
if (depth > maxDepth)
|
||||
{
|
||||
if (allowNested)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "applyTransaction: Invalid SigningAccount.Account.";
|
||||
<< "checkMultiSign: Multi-signing depth limit exceeded.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
}
|
||||
if (iter->account != txSignerAcctID)
|
||||
{
|
||||
// The SigningAccount is not in the SignerEntries.
|
||||
JLOG(ctx.j.trace())
|
||||
<< "applyTransaction: Invalid SigningAccount.Account.";
|
||||
return tefBAD_SIGNATURE;
|
||||
|
||||
JLOG(ctx.j.warn())
|
||||
<< "checkMultiSign: Nested multisigning disabled.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// We found the SigningAccount in the list of valid signers. Now we
|
||||
// need to compute the accountID that is associated with the signer's
|
||||
// public key.
|
||||
auto const spk = txSigner.getFieldVL(sfSigningPubKey);
|
||||
ancestors.insert(acc);
|
||||
|
||||
if (!publicKeyType(makeSlice(spk)))
|
||||
// Get the SignerList for the account we're validating signers for
|
||||
std::shared_ptr<STLedgerEntry const> sleAllowedSigners =
|
||||
ctx.view.read(keylet::signers(acc));
|
||||
|
||||
if (!sleAllowedSigners)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: signing public key type is unknown";
|
||||
return tefBAD_SIGNATURE;
|
||||
JLOG(ctx.j.trace()) << "checkMultiSign: Account " << acc
|
||||
<< " not set up for multi-signing.";
|
||||
return tefNOT_MULTI_SIGNING;
|
||||
}
|
||||
|
||||
AccountID const signingAcctIDFromPubKey =
|
||||
calcAccountID(PublicKey(makeSlice(spk)));
|
||||
uint32_t const quorum = sleAllowedSigners->getFieldU32(sfSignerQuorum);
|
||||
uint32_t sum{0};
|
||||
|
||||
// Verify that the signingAcctID and the signingAcctIDFromPubKey
|
||||
// belong together. Here is are the rules:
|
||||
//
|
||||
// 1. "Phantom account": an account that is not in the ledger
|
||||
// A. If signingAcctID == signingAcctIDFromPubKey and the
|
||||
// signingAcctID is not in the ledger then we have a phantom
|
||||
// account.
|
||||
// B. Phantom accounts are always allowed as multi-signers.
|
||||
//
|
||||
// 2. "Master Key"
|
||||
// A. signingAcctID == signingAcctIDFromPubKey, and signingAcctID
|
||||
// is in the ledger.
|
||||
// B. If the signingAcctID in the ledger does not have the
|
||||
// asfDisableMaster flag set, then the signature is allowed.
|
||||
//
|
||||
// 3. "Regular Key"
|
||||
// A. signingAcctID != signingAcctIDFromPubKey, and signingAcctID
|
||||
// is in the ledger.
|
||||
// B. If signingAcctIDFromPubKey == signingAcctID.RegularKey (from
|
||||
// ledger) then the signature is allowed.
|
||||
//
|
||||
// No other signatures are allowed. (January 2015)
|
||||
auto allowedSigners =
|
||||
SignerEntries::deserialize(*sleAllowedSigners, ctx.j, "ledger");
|
||||
if (!allowedSigners)
|
||||
return allowedSigners.error();
|
||||
|
||||
// In any of these cases we need to know whether the account is in
|
||||
// the ledger. Determine that now.
|
||||
auto sleTxSignerRoot = ctx.view.read(keylet::account(txSignerAcctID));
|
||||
|
||||
if (signingAcctIDFromPubKey == txSignerAcctID)
|
||||
// Build lookup map for O(1) signer validation and weight retrieval
|
||||
std::map<AccountID, uint16_t> signerWeights;
|
||||
uint32_t totalWeight{0}, cyclicWeight{0};
|
||||
for (auto const& entry : *allowedSigners)
|
||||
{
|
||||
// Either Phantom or Master. Phantoms automatically pass.
|
||||
if (sleTxSignerRoot)
|
||||
signerWeights[entry.account] = entry.weight;
|
||||
totalWeight += entry.weight;
|
||||
if (ancestors.count(entry.account))
|
||||
cyclicWeight += entry.weight;
|
||||
}
|
||||
|
||||
// Walk the signers array, validating each signer
|
||||
// Signers must be in strict ascending order for consensus
|
||||
std::optional<AccountID> prevSigner;
|
||||
|
||||
for (auto const& signerEntry : signers)
|
||||
{
|
||||
AccountID const signer = signerEntry.getAccountID(sfAccount);
|
||||
bool const isNested = signerEntry.isFieldPresent(sfSigners);
|
||||
|
||||
// Enforce strict ascending order (required for consensus)
|
||||
if (prevSigner && signer <= *prevSigner)
|
||||
{
|
||||
// Master Key. Account may not have asfDisableMaster set.
|
||||
std::uint32_t const signerAccountFlags =
|
||||
sleTxSignerRoot->getFieldU32(sfFlags);
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Signers not in strict ascending order: "
|
||||
<< signer << " <= " << *prevSigner;
|
||||
return temMALFORMED;
|
||||
}
|
||||
prevSigner = signer;
|
||||
|
||||
if (signerAccountFlags & lsfDisableMaster)
|
||||
// Skip cyclic signers - they cannot contribute at this level
|
||||
if (ancestors.count(signer))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Skipping cyclic signer: " << signer;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lookup signer in authorized set
|
||||
auto const weightIt = signerWeights.find(signer);
|
||||
if (weightIt == signerWeights.end())
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Invalid signer " << signer
|
||||
<< " not in signer list for " << acc;
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
uint16_t const weight = weightIt->second;
|
||||
|
||||
// Check if this signer has nested signers (delegation)
|
||||
if (isNested)
|
||||
{
|
||||
// This is a nested multi-signer that delegates to sub-signers
|
||||
if (signerEntry.isFieldPresent(sfSigningPubKey) ||
|
||||
signerEntry.isFieldPresent(sfTxnSignature))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "checkMultiSign: Signer " << signer
|
||||
<< " cannot have both nested signers "
|
||||
"and signature fields.";
|
||||
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, ancestors);
|
||||
if (!isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
// Nested signers met their quorum - add this signer's weight
|
||||
sum += weight;
|
||||
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Nested signer " << signer
|
||||
<< " validated, weight=" << weight << ", depth=" << depth
|
||||
<< ", sum=" << sum << "/" << quorum;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is a leaf signer - validate signature
|
||||
if (!signerEntry.isFieldPresent(sfSigningPubKey) ||
|
||||
!signerEntry.isFieldPresent(sfTxnSignature))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "applyTransaction: Signer:Account lsfDisableMaster.";
|
||||
return tefMASTER_DISABLED;
|
||||
<< "checkMultiSign: Leaf signer " << signer
|
||||
<< " must have SigningPubKey and TxnSignature.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
auto const spk = signerEntry.getFieldVL(sfSigningPubKey);
|
||||
|
||||
if (!publicKeyType(makeSlice(spk)))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Unknown public key type for signer "
|
||||
<< signer;
|
||||
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())
|
||||
<< "checkMultiSign: Signer " << signer
|
||||
<< " has lsfDisableMaster set.";
|
||||
return tefMASTER_DISABLED;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!sleTxSignerRoot)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Non-phantom signer " << signer
|
||||
<< " lacks account root.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
if (!sleTxSignerRoot->isFieldPresent(sfRegularKey))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "checkMultiSign: Signer "
|
||||
<< signer << " lacks RegularKey.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
if (signingAcctIDFromPubKey !=
|
||||
sleTxSignerRoot->getAccountID(sfRegularKey))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Signer " << signer
|
||||
<< " pubkey doesn't match RegularKey.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
}
|
||||
|
||||
// Valid leaf signer - add their weight
|
||||
sum += weight;
|
||||
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Leaf signer " << signer
|
||||
<< " validated, weight=" << weight << ", depth=" << depth
|
||||
<< ", sum=" << sum << "/" << quorum;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
// Calculate effective quorum, relaxing for cyclic lockout scenarios
|
||||
// Sanity check: cyclicWeight must not exceed totalWeight (underflow
|
||||
// guard)
|
||||
if (cyclicWeight > totalWeight)
|
||||
{
|
||||
// May be a Regular Key. Let's find out.
|
||||
// Public key must hash to the account's regular key.
|
||||
if (!sleTxSignerRoot)
|
||||
{
|
||||
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;
|
||||
}
|
||||
JLOG(ctx.j.error()) << "checkMultiSign: Invariant violation for "
|
||||
<< acc << ": cyclicWeight (" << cyclicWeight
|
||||
<< ") > totalWeight (" << totalWeight << ")";
|
||||
return tefINTERNAL;
|
||||
}
|
||||
// The signer is legitimate. Add their weight toward the quorum.
|
||||
weightSum += iter->weight;
|
||||
}
|
||||
|
||||
// Cannot perform transaction if quorum is not met.
|
||||
if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum))
|
||||
uint32_t effectiveQuorum = quorum;
|
||||
uint32_t const maxAchievable = totalWeight - cyclicWeight;
|
||||
|
||||
if (cyclicWeight > 0 && maxAchievable < quorum)
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "checkMultiSign: Cyclic lockout detected for " << acc
|
||||
<< ": relaxing quorum from " << quorum << " to "
|
||||
<< maxAchievable << " (total=" << totalWeight
|
||||
<< ", cyclic=" << cyclicWeight << ")";
|
||||
effectiveQuorum = maxAchievable;
|
||||
}
|
||||
|
||||
// Sanity check: effectiveQuorum of 0 means all signers are cyclic -
|
||||
// this is an irrecoverable misconfiguration
|
||||
if (effectiveQuorum == 0)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "checkMultiSign: All signers for " << acc
|
||||
<< " are cyclic - no valid signing path exists.";
|
||||
return tefBAD_QUORUM;
|
||||
}
|
||||
|
||||
// Check if accumulated weight meets required quorum
|
||||
if (sum < effectiveQuorum)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "checkMultiSign: Quorum not met for " << acc
|
||||
<< " at depth " << depth << " (sum=" << sum
|
||||
<< ", required=" << effectiveQuorum << ")";
|
||||
return tefBAD_QUORUM;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
};
|
||||
|
||||
STArray const& entries(ctx.tx.getFieldArray(sfSigners));
|
||||
|
||||
// Initial call with empty ancestor set - the function inserts acc after
|
||||
// cycle check
|
||||
NotTEC result = validateSigners(id, entries, 1, {});
|
||||
if (!isTesSuccess(result))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "applyTransaction: Signers failed to meet quorum.";
|
||||
return tefBAD_QUORUM;
|
||||
<< "checkMultiSign: Validation failed with " << transToken(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Met the quorum. Continue.
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -374,8 +374,6 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttEXPORT_SIGN:
|
||||
case ttEXPORT:
|
||||
return Change::calculateBaseFee(view, tx);
|
||||
case ttNFTOKEN_MINT:
|
||||
return NFTokenMint::calculateBaseFee(view, tx);
|
||||
@@ -546,8 +544,6 @@ invoke_apply(ApplyContext& ctx)
|
||||
case ttFEE:
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttEXPORT:
|
||||
case ttEXPORT_SIGN:
|
||||
case ttEMIT_FAILURE: {
|
||||
Change p(ctx);
|
||||
return p();
|
||||
|
||||
@@ -484,44 +484,61 @@ OverlayImpl::start()
|
||||
m_peerFinder->setConfig(config);
|
||||
m_peerFinder->start();
|
||||
|
||||
auto addIps = [&](std::vector<std::string> bootstrapIps) -> void {
|
||||
auto addIps = [this](std::vector<std::string> ips, bool fixed) {
|
||||
beast::Journal const& j = app_.journal("Overlay");
|
||||
for (auto& ip : bootstrapIps)
|
||||
for (auto& ip : ips)
|
||||
{
|
||||
std::size_t pos = ip.find('#');
|
||||
if (pos != std::string::npos)
|
||||
ip.erase(pos);
|
||||
|
||||
JLOG(j.trace()) << "Found boostrap IP: " << ip;
|
||||
JLOG(j.trace())
|
||||
<< "Found " << (fixed ? "fixed" : "bootstrap") << " IP: " << ip;
|
||||
}
|
||||
|
||||
m_resolver.resolve(
|
||||
bootstrapIps,
|
||||
[&](std::string const& name,
|
||||
ips,
|
||||
[this, fixed](
|
||||
std::string const& name,
|
||||
std::vector<beast::IP::Endpoint> const& addresses) {
|
||||
std::vector<std::string> ips;
|
||||
ips.reserve(addresses.size());
|
||||
beast::Journal const& j = app_.journal("Overlay");
|
||||
std::string const base("config: ");
|
||||
|
||||
std::vector<beast::IP::Endpoint> eps;
|
||||
eps.reserve(addresses.size());
|
||||
for (auto const& addr : addresses)
|
||||
{
|
||||
std::string addrStr = addr.port() == 0
|
||||
? to_string(addr.at_port(DEFAULT_PEER_PORT))
|
||||
: to_string(addr);
|
||||
JLOG(j.trace()) << "Parsed boostrap IP: " << addrStr;
|
||||
ips.push_back(addrStr);
|
||||
auto ep = addr.port() == 0 ? addr.at_port(DEFAULT_PEER_PORT)
|
||||
: addr;
|
||||
JLOG(j.trace())
|
||||
<< "Parsed " << (fixed ? "fixed" : "bootstrap")
|
||||
<< " IP: " << ep;
|
||||
eps.push_back(ep);
|
||||
}
|
||||
|
||||
std::string const base("config: ");
|
||||
if (!ips.empty())
|
||||
m_peerFinder->addFallbackStrings(base + name, ips);
|
||||
if (eps.empty())
|
||||
return;
|
||||
|
||||
if (fixed)
|
||||
{
|
||||
m_peerFinder->addFixedPeer(base + name, eps);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::vector<std::string> strs;
|
||||
strs.reserve(eps.size());
|
||||
for (auto const& ep : eps)
|
||||
strs.push_back(to_string(ep));
|
||||
m_peerFinder->addFallbackStrings(base + name, strs);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!app_.config().IPS.empty())
|
||||
addIps(app_.config().IPS);
|
||||
addIps(app_.config().IPS, false);
|
||||
|
||||
if (!app_.config().IPS_FIXED.empty())
|
||||
addIps(app_.config().IPS_FIXED);
|
||||
addIps(app_.config().IPS_FIXED, true);
|
||||
|
||||
auto const timer = std::make_shared<Timer>(*this);
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
@@ -378,7 +378,7 @@ extern uint256 const fixInvalidTxFlags;
|
||||
extern uint256 const featureExtendedHookState;
|
||||
extern uint256 const fixCronStacking;
|
||||
extern uint256 const fixHookAPI20251128;
|
||||
extern uint256 const featureExport;
|
||||
extern uint256 const featureNestedMultiSign;
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
@@ -56,15 +56,9 @@ namespace keylet {
|
||||
Keylet const&
|
||||
emittedDir() noexcept;
|
||||
|
||||
Keylet const&
|
||||
exportedDir() noexcept;
|
||||
|
||||
Keylet
|
||||
emittedTxn(uint256 const& id) noexcept;
|
||||
|
||||
Keylet
|
||||
exportedTxn(uint256 const& id) noexcept;
|
||||
|
||||
Keylet
|
||||
hookDefinition(uint256 const& hash) noexcept;
|
||||
|
||||
|
||||
@@ -260,8 +260,6 @@ enum LedgerEntryType : std::uint16_t
|
||||
\sa keylet::emitted
|
||||
*/
|
||||
ltEMITTED_TXN = 'E',
|
||||
|
||||
ltEXPORTED_TXN = 0x4578, // Ex (exported transaction)
|
||||
};
|
||||
// clang-format off
|
||||
|
||||
@@ -320,8 +318,7 @@ enum LedgerSpecificFlags {
|
||||
// ltDIR_NODE
|
||||
lsfNFTokenBuyOffers = 0x00000001,
|
||||
lsfNFTokenSellOffers = 0x00000002,
|
||||
lsfEmittedDir = 0x00000004,
|
||||
lsfExportedDir = 0x00000008,
|
||||
lsfEmittedDir = 0x00000004,
|
||||
|
||||
// ltNFTOKEN_OFFER
|
||||
lsfSellNFToken = 0x00000001,
|
||||
|
||||
@@ -355,7 +355,6 @@ extern SF_UINT16 const sfHookEmitCount;
|
||||
extern SF_UINT16 const sfHookExecutionIndex;
|
||||
extern SF_UINT16 const sfHookApiVersion;
|
||||
extern SF_UINT16 const sfHookStateScale;
|
||||
extern SF_UINT16 const sfHookExportCount;
|
||||
|
||||
// 32-bit integers (common)
|
||||
extern SF_UINT32 const sfNetworkID;
|
||||
@@ -596,7 +595,6 @@ extern SField const sfSigner;
|
||||
extern SField const sfMajority;
|
||||
extern SField const sfDisabledValidator;
|
||||
extern SField const sfEmittedTxn;
|
||||
extern SField const sfExportedTxn;
|
||||
extern SField const sfHookExecution;
|
||||
extern SField const sfHookDefinition;
|
||||
extern SField const sfHookParameter;
|
||||
|
||||
@@ -67,7 +67,6 @@ enum TELcodes : TERUnderlyingType {
|
||||
telNON_LOCAL_EMITTED_TXN,
|
||||
telIMPORT_VL_KEY_NOT_RECOGNISED,
|
||||
telCAN_NOT_QUEUE_IMPORT,
|
||||
telSHADOW_TICKET_REQUIRED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -149,12 +149,6 @@ enum TxType : std::uint16_t
|
||||
ttURITOKEN_CREATE_SELL_OFFER = 48,
|
||||
ttURITOKEN_CANCEL_SELL_OFFER = 49,
|
||||
|
||||
/* A pseudo-txn containing an exported transaction plus signatures from the validators */
|
||||
ttEXPORT = 90,
|
||||
|
||||
/* A pseudo-txn containing a validator's signature for an export transaction */
|
||||
ttEXPORT_SIGN = 91,
|
||||
|
||||
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
|
||||
ttCRON = 92,
|
||||
|
||||
|
||||
@@ -484,7 +484,7 @@ REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(Export, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(NestedMultiSign, Supported::yes, VoteBehavior::DefaultNo);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
// because they could potentially get enabled.
|
||||
|
||||
@@ -66,8 +66,6 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
HOOK_DEFINITION = 'D',
|
||||
EMITTED_TXN = 'E',
|
||||
EMITTED_DIR = 'F',
|
||||
EXPORTED_TXN = 0x4578, // Ex
|
||||
EXPORTED_DIR = 0x4564, // Ed
|
||||
NFTOKEN_OFFER = 'q',
|
||||
NFTOKEN_BUY_OFFERS = 'h',
|
||||
NFTOKEN_SELL_OFFERS = 'i',
|
||||
@@ -149,14 +147,6 @@ emittedDir() noexcept
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet const&
|
||||
exportedDir() noexcept
|
||||
{
|
||||
static Keylet const ret{
|
||||
ltDIR_NODE, indexHash(LedgerNameSpace::EXPORTED_DIR)};
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet
|
||||
hookStateDir(AccountID const& id, uint256 const& ns) noexcept
|
||||
{
|
||||
@@ -169,12 +159,6 @@ emittedTxn(uint256 const& id) noexcept
|
||||
return {ltEMITTED_TXN, indexHash(LedgerNameSpace::EMITTED_TXN, id)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
exportedTxn(uint256 const& id) noexcept
|
||||
{
|
||||
return {ltEXPORTED_TXN, indexHash(LedgerNameSpace::EXPORTED_TXN, id)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
hook(AccountID const& id) noexcept
|
||||
{
|
||||
|
||||
@@ -44,8 +44,9 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
sfSigner.getCode(),
|
||||
{
|
||||
{sfAccount, soeREQUIRED},
|
||||
{sfSigningPubKey, soeREQUIRED},
|
||||
{sfTxnSignature, soeREQUIRED},
|
||||
{sfSigningPubKey, soeOPTIONAL},
|
||||
{sfTxnSignature, soeOPTIONAL},
|
||||
{sfSigners, soeOPTIONAL},
|
||||
});
|
||||
|
||||
add(sfMajority.jsonName.c_str(),
|
||||
|
||||
@@ -380,15 +380,6 @@ LedgerFormats::LedgerFormats()
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED}
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::ExportedTxn,
|
||||
ltEXPORTED_TXN,
|
||||
{
|
||||
{sfExportedTxn, soeOPTIONAL},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ CONSTRUCT_TYPED_SFIELD(sfHookEmitCount, "HookEmitCount", UINT16,
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookExecutionIndex, "HookExecutionIndex", UINT16, 19);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookApiVersion, "HookApiVersion", UINT16, 20);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookStateScale, "HookStateScale", UINT16, 21);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookExportCount, "HookExportCount", UINT16, 22);
|
||||
|
||||
// 32-bit integers (common)
|
||||
CONSTRUCT_TYPED_SFIELD(sfNetworkID, "NetworkID", UINT32, 1);
|
||||
@@ -362,7 +361,6 @@ CONSTRUCT_UNTYPED_SFIELD(sfImportVLKey, "ImportVLKey", OBJECT,
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfHookEmission, "HookEmission", OBJECT, 93);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfMintURIToken, "MintURIToken", OBJECT, 92);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfAmountEntry, "AmountEntry", OBJECT, 91);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfExportedTxn, "ExportedTxn", OBJECT, 90);
|
||||
|
||||
// array of objects
|
||||
// ARRAY/1 is reserved for end of array
|
||||
|
||||
@@ -369,64 +369,124 @@ STTx::checkMultiSign(
|
||||
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
|
||||
(requireCanonicalSig == RequireFullyCanonicalSig::yes);
|
||||
|
||||
// Signers must be in sorted order by AccountID.
|
||||
AccountID lastAccountID(beast::zero);
|
||||
|
||||
bool const isWildcardNetwork =
|
||||
isFieldPresent(sfNetworkID) && getFieldU32(sfNetworkID) == 65535;
|
||||
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
auto const accountID = signer.getAccountID(sfAccount);
|
||||
// Set max depth based on feature flag
|
||||
int const maxDepth = rules.enabled(featureNestedMultiSign) ? 4 : 1;
|
||||
|
||||
// The account owner may not multisign for themselves.
|
||||
if (accountID == txnAccountID)
|
||||
return Unexpected("Invalid multisigner.");
|
||||
// Define recursive lambda for checking signatures at any depth
|
||||
std::function<Expected<void, std::string>(
|
||||
STArray const&, AccountID const&, int)>
|
||||
checkSignersArray;
|
||||
|
||||
// No duplicate signers allowed.
|
||||
if (lastAccountID == accountID)
|
||||
return Unexpected("Duplicate Signers not allowed.");
|
||||
checkSignersArray = [&](STArray const& signersArray,
|
||||
AccountID const& parentAccountID,
|
||||
int depth) -> Expected<void, std::string> {
|
||||
// Check depth limit
|
||||
if (depth > maxDepth)
|
||||
return Unexpected("Multi-signing depth limit exceeded.");
|
||||
|
||||
// Accounts must be in order by account ID. No duplicates allowed.
|
||||
if (lastAccountID > accountID)
|
||||
return Unexpected("Unsorted Signers array.");
|
||||
// There are well known bounds that the number of signers must be
|
||||
// within.
|
||||
if (signersArray.size() < minMultiSigners ||
|
||||
signersArray.size() > maxMultiSigners(&rules))
|
||||
return Unexpected("Invalid Signers array size.");
|
||||
|
||||
// The next signature must be greater than this one.
|
||||
lastAccountID = accountID;
|
||||
// Signers must be in sorted order by AccountID.
|
||||
AccountID lastAccountID(beast::zero);
|
||||
|
||||
// Verify the signature.
|
||||
bool validSig = false;
|
||||
try
|
||||
for (auto const& signer : signersArray)
|
||||
{
|
||||
Serializer s = dataStart;
|
||||
finishMultiSigningData(accountID, s);
|
||||
auto const accountID = signer.getAccountID(sfAccount);
|
||||
|
||||
auto spk = signer.getFieldVL(sfSigningPubKey);
|
||||
// The account owner may not multisign for themselves.
|
||||
if (accountID == txnAccountID)
|
||||
return Unexpected("Invalid multisigner.");
|
||||
|
||||
if (publicKeyType(makeSlice(spk)))
|
||||
// No duplicate signers allowed.
|
||||
if (lastAccountID == accountID)
|
||||
return Unexpected("Duplicate Signers not allowed.");
|
||||
|
||||
// Accounts must be in order by account ID. No duplicates allowed.
|
||||
if (lastAccountID > accountID)
|
||||
return Unexpected("Unsorted Signers array.");
|
||||
|
||||
// The next signature must be greater than this one.
|
||||
lastAccountID = accountID;
|
||||
|
||||
// Check if this signer has nested signers
|
||||
if (signer.isFieldPresent(sfSigners))
|
||||
{
|
||||
Blob const signature = signer.getFieldVL(sfTxnSignature);
|
||||
// This is a nested multi-signer
|
||||
if (maxDepth == 1)
|
||||
{
|
||||
// amendment is not enabled, this is an error
|
||||
return Unexpected("FeatureNestedMultiSign is disabled");
|
||||
}
|
||||
|
||||
// wildcard network gets a free pass
|
||||
validSig = isWildcardNetwork ||
|
||||
verify(PublicKey(makeSlice(spk)),
|
||||
s.slice(),
|
||||
makeSlice(signature),
|
||||
fullyCanonical);
|
||||
// Ensure it doesn't also have signature fields
|
||||
if (signer.isFieldPresent(sfSigningPubKey) ||
|
||||
signer.isFieldPresent(sfTxnSignature))
|
||||
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))
|
||||
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)
|
||||
return Unexpected(
|
||||
std::string("Invalid signature on account ") +
|
||||
toBase58(accountID) + ".");
|
||||
}
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
// We assume any problem lies with the signature.
|
||||
validSig = false;
|
||||
}
|
||||
if (!validSig)
|
||||
return Unexpected(
|
||||
std::string("Invalid signature on account ") +
|
||||
toBase58(accountID) + ".");
|
||||
}
|
||||
// All signatures verified.
|
||||
return {};
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
// Start the recursive check at depth 1
|
||||
return checkSignersArray(signers, txnAccountID, 1);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -141,7 +141,6 @@ transResults()
|
||||
MAKE_ERROR(telNON_LOCAL_EMITTED_TXN, "Emitted transaction cannot be applied because it was not generated locally."),
|
||||
MAKE_ERROR(telIMPORT_VL_KEY_NOT_RECOGNISED, "Import vl key was not recognized."),
|
||||
MAKE_ERROR(telCAN_NOT_QUEUE_IMPORT, "Import transaction was not able to be directly applied and cannot be queued."),
|
||||
MAKE_ERROR(telSHADOW_TICKET_REQUIRED, "The imported transaction uses a TicketSequence but no shadow ticket exists."),
|
||||
MAKE_ERROR(temMALFORMED, "Malformed transaction."),
|
||||
MAKE_ERROR(temBAD_AMOUNT, "Can only send positive amounts."),
|
||||
MAKE_ERROR(temBAD_CURRENCY, "Malformed: Bad currency."),
|
||||
|
||||
@@ -490,26 +490,6 @@ TxFormats::TxFormats()
|
||||
{sfStartTime, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::ExportSign,
|
||||
ttEXPORT_SIGN,
|
||||
{
|
||||
{sfSigner, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfTransactionHash, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Export,
|
||||
ttEXPORT,
|
||||
{
|
||||
{sfTransactionHash, soeREQUIRED},
|
||||
{sfExportedTxn, soeREQUIRED},
|
||||
{sfSigners, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -140,9 +140,6 @@ JSS(HookState); // ledger type.
|
||||
JSS(HookStateData); // field.
|
||||
JSS(HookStateKey); // field.
|
||||
JSS(EmittedTxn); // ledger type.
|
||||
JSS(ExportedTxn);
|
||||
JSS(Export);
|
||||
JSS(ExportSign);
|
||||
JSS(SignerList); // ledger type.
|
||||
JSS(SignerListSet); // transaction type.
|
||||
JSS(SigningPubKey); // field.
|
||||
|
||||
@@ -1183,12 +1183,21 @@ transactionSubmitMultiSigned(
|
||||
// The Signers array may only contain Signer objects.
|
||||
if (std::find_if_not(
|
||||
signers.begin(), signers.end(), [](STObject const& obj) {
|
||||
return (
|
||||
// A Signer object always contains these fields and no
|
||||
// others.
|
||||
obj.isFieldPresent(sfAccount) &&
|
||||
obj.isFieldPresent(sfSigningPubKey) &&
|
||||
obj.isFieldPresent(sfTxnSignature) && obj.getCount() == 3);
|
||||
if (obj.getCount() != 4 || !obj.isFieldPresent(sfAccount))
|
||||
return false;
|
||||
// leaf signer
|
||||
if (obj.isFieldPresent(sfSigningPubKey) &&
|
||||
obj.isFieldPresent(sfTxnSignature) &&
|
||||
!obj.isFieldPresent(sfSigners))
|
||||
return true;
|
||||
|
||||
// nested signer
|
||||
if (!obj.isFieldPresent(sfSigningPubKey) &&
|
||||
!obj.isFieldPresent(sfTxnSignature) &&
|
||||
obj.isFieldPresent(sfSigners))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}) != signers.end())
|
||||
{
|
||||
return RPC::make_param_error(
|
||||
|
||||
@@ -1659,6 +1659,800 @@ public:
|
||||
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};
|
||||
// Env env{*this, envconfig(), features, nullptr,
|
||||
// beast::severities::kTrace};
|
||||
|
||||
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();
|
||||
|
||||
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}, {shade, 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(fiona), // weight 1
|
||||
msigner(
|
||||
edgar, msigner(bogie), msigner(shade)), // weight 2
|
||||
msigner(
|
||||
henry, // weight 2
|
||||
msigner(becky, msigner(bogie), msigner(demon)))}),
|
||||
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(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(bogie),
|
||||
msigner(shade))}),
|
||||
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);
|
||||
}
|
||||
|
||||
// Test Case 6: Simple cycle detection (A -> B -> A)
|
||||
{
|
||||
testcase("Cycle Detection - Simple");
|
||||
|
||||
// Reset signer lists for clean state
|
||||
env(signers(alice, jtx::none));
|
||||
env(signers(becky, jtx::none));
|
||||
env.close();
|
||||
|
||||
// becky's signer list includes alice
|
||||
// alice's signer list includes becky
|
||||
// This creates: alice -> becky -> alice (cycle)
|
||||
env(signers(alice, 1, {{becky, 1}, {bogie, 1}}));
|
||||
env(signers(becky, 1, {{alice, 1}, {demon, 1}}));
|
||||
env.close();
|
||||
|
||||
// Without cycle relaxation this would fail because:
|
||||
// - alice needs becky (weight 1)
|
||||
// - becky needs alice, but alice is ancestor -> cycle
|
||||
// - becky's effective quorum relaxes since alice is unavailable
|
||||
// - demon can satisfy becky's relaxed quorum
|
||||
std::uint32_t aliceSeq = env.seq(alice);
|
||||
env(noop(alice),
|
||||
msig({msigner(becky, msigner(demon))}),
|
||||
L(),
|
||||
fee(4 * baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
|
||||
|
||||
// Test that direct signer still works normally
|
||||
aliceSeq = env.seq(alice);
|
||||
env(noop(alice), msig({msigner(bogie)}), L(), fee(3 * baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
|
||||
}
|
||||
|
||||
// Test Case 7: The specific lockout scenario
|
||||
// onyx:{jade, nova:{ruby:{jade, nova}, jade}}
|
||||
// All have quorum 2, only jade can actually sign
|
||||
{
|
||||
testcase("Cycle Detection - Complex Lockout");
|
||||
|
||||
Account const onyx{"onyx", KeyType::secp256k1};
|
||||
Account const nova{"nova", KeyType::ed25519};
|
||||
Account const ruby{"ruby", KeyType::secp256k1};
|
||||
Account const jade{"jade", KeyType::ed25519}; // phantom signer
|
||||
|
||||
env.fund(XRP(1000), onyx, nova, ruby);
|
||||
env.close();
|
||||
|
||||
// Set up signer lists FIRST (before disabling master keys)
|
||||
// ruby: {jade, nova} with quorum 2
|
||||
env(signers(ruby, 2, {{jade, 1}, {nova, 1}}));
|
||||
// nova: {ruby, jade} with quorum 2
|
||||
env(signers(nova, 2, {{jade, 1}, {ruby, 1}}));
|
||||
// onyx: {jade, nova} with quorum 2
|
||||
env(signers(onyx, 2, {{jade, 1}, {nova, 1}}));
|
||||
env.close();
|
||||
|
||||
// NOW disable master keys (signer lists provide alternative)
|
||||
env(fset(onyx, asfDisableMaster), sig(onyx));
|
||||
env(fset(nova, asfDisableMaster), sig(nova));
|
||||
env(fset(ruby, asfDisableMaster), sig(ruby));
|
||||
env.close();
|
||||
|
||||
// The signing tree for onyx:
|
||||
// onyx (quorum 2) -> jade (weight 1) + nova (weight 1)
|
||||
// nova (quorum 2) -> jade (weight 1) + ruby (weight 1)
|
||||
// ruby (quorum 2) -> jade (weight 1) + nova (weight 1, CYCLE!)
|
||||
//
|
||||
// Without cycle detection: ruby needs nova, but nova is ancestor ->
|
||||
// stuck With cycle detection:
|
||||
// - At ruby level: nova is cyclic, cyclicWeight=1, totalWeight=2
|
||||
// - maxAchievable = 2-1 = 1 < quorum(2), so effectiveQuorum -> 1
|
||||
// - jade alone can satisfy ruby's relaxed quorum
|
||||
// - ruby satisfied -> nova gets ruby's weight
|
||||
// - nova: jade(1) + ruby(1) = 2 >= quorum(2) ✓
|
||||
// - onyx: jade(1) + nova(1) = 2 >= quorum(2) ✓
|
||||
|
||||
std::uint32_t onyxSeq = env.seq(onyx);
|
||||
env(noop(onyx),
|
||||
msig(
|
||||
{msigner(jade),
|
||||
msigner(
|
||||
nova,
|
||||
msigner(jade),
|
||||
msigner(
|
||||
ruby, msigner(jade)))}), // nova is cyclic,
|
||||
// skipped at ruby level
|
||||
L(),
|
||||
fee(6 * baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(onyx) == onyxSeq + 1);
|
||||
}
|
||||
|
||||
// Test Case 8: Cycle where all signers are cyclic (effectiveQuorum ==
|
||||
// 0)
|
||||
{
|
||||
testcase("Cycle Detection - Total Lockout");
|
||||
|
||||
Account const alpha{"alpha", KeyType::secp256k1};
|
||||
Account const beta{"beta", KeyType::ed25519};
|
||||
Account const gamma{"gamma", KeyType::secp256k1};
|
||||
|
||||
env.fund(XRP(1000), alpha, beta, gamma);
|
||||
env.close();
|
||||
|
||||
// Set up pure cycle signer lists FIRST
|
||||
env(signers(alpha, 1, {{beta, 1}}));
|
||||
env(signers(beta, 1, {{gamma, 1}}));
|
||||
env(signers(gamma, 1, {{alpha, 1}}));
|
||||
env.close();
|
||||
|
||||
// NOW disable master keys
|
||||
env(fset(alpha, asfDisableMaster), sig(alpha));
|
||||
env(fset(beta, asfDisableMaster), sig(beta));
|
||||
env(fset(gamma, asfDisableMaster), sig(gamma));
|
||||
env.close();
|
||||
|
||||
// This is a true lockout - no valid signing path exists.
|
||||
// gamma appears as a leaf signer but has master disabled ->
|
||||
// tefMASTER_DISABLED (The cycle detection would return
|
||||
// tefBAD_QUORUM if gamma were nested, but there's no way to
|
||||
// construct such a transaction since gamma's only signer is alpha,
|
||||
// which is what we're trying to sign for)
|
||||
std::uint32_t alphaSeq = env.seq(alpha);
|
||||
env(noop(alpha),
|
||||
msig({msigner(
|
||||
beta,
|
||||
msigner(gamma))}), // gamma can't sign - master disabled
|
||||
L(),
|
||||
fee(4 * baseFee),
|
||||
ter(tefMASTER_DISABLED));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alpha) == alphaSeq);
|
||||
}
|
||||
|
||||
// Test Case 9: Cycle at depth 3 (near max depth)
|
||||
{
|
||||
testcase("Cycle Detection - Deep Cycle");
|
||||
|
||||
// Reset signer lists
|
||||
env(signers(alice, jtx::none));
|
||||
env(signers(becky, jtx::none));
|
||||
env(signers(cheri, jtx::none));
|
||||
env(signers(daria, jtx::none));
|
||||
env.close();
|
||||
|
||||
// Structure: alice -> becky -> cheri -> daria -> alice (cycle at
|
||||
// depth 4)
|
||||
env(signers(alice, 1, {{becky, 1}, {bogie, 1}}));
|
||||
env(signers(becky, 1, {{cheri, 1}}));
|
||||
env(signers(cheri, 1, {{daria, 1}}));
|
||||
env(signers(daria, 1, {{alice, 1}, {demon, 1}}));
|
||||
env.close();
|
||||
|
||||
// At depth 4, daria needs alice but alice is ancestor
|
||||
// daria's quorum relaxes, demon can satisfy
|
||||
std::uint32_t aliceSeq = env.seq(alice);
|
||||
env(noop(alice),
|
||||
msig({msigner(
|
||||
becky, msigner(cheri, msigner(daria, msigner(demon))))}),
|
||||
L(),
|
||||
fee(6 * baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
|
||||
}
|
||||
|
||||
// Test Case 10: Multiple independent cycles in same tree
|
||||
{
|
||||
testcase("Cycle Detection - Multiple Cycles");
|
||||
|
||||
// Reset signer lists
|
||||
env(signers(alice, jtx::none));
|
||||
env(signers(becky, jtx::none));
|
||||
env(signers(cheri, jtx::none));
|
||||
env.close();
|
||||
|
||||
// alice -> {becky, cheri}
|
||||
// becky -> {alice, bogie} (cycle back to alice)
|
||||
// cheri -> {alice, demon} (another cycle back to alice)
|
||||
env(signers(alice, 2, {{becky, 1}, {cheri, 1}}));
|
||||
env(signers(becky, 2, {{alice, 1}, {bogie, 1}}));
|
||||
env(signers(cheri, 2, {{alice, 1}, {demon, 1}}));
|
||||
env.close();
|
||||
|
||||
// Both becky and cheri have cycles back to alice
|
||||
// Both need their quorums relaxed
|
||||
// bogie satisfies becky, demon satisfies cheri
|
||||
std::uint32_t aliceSeq = env.seq(alice);
|
||||
env(noop(alice),
|
||||
msig(
|
||||
{msigner(becky, msigner(bogie)),
|
||||
msigner(cheri, msigner(demon))}),
|
||||
L(),
|
||||
fee(6 * baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
|
||||
}
|
||||
|
||||
// Test Case 11: Cycle with sufficient non-cyclic weight (no relaxation
|
||||
// needed)
|
||||
{
|
||||
testcase("Cycle Detection - No Relaxation Needed");
|
||||
|
||||
// Reset signer lists
|
||||
env(signers(alice, jtx::none));
|
||||
env(signers(becky, jtx::none));
|
||||
env.close();
|
||||
|
||||
// becky has alice in signer list but also has enough other signers
|
||||
env(signers(alice, 1, {{becky, 1}}));
|
||||
env(signers(becky, 2, {{alice, 1}, {bogie, 1}, {demon, 1}}));
|
||||
env.close();
|
||||
|
||||
// becky quorum is 2, alice is cyclic (weight 1)
|
||||
// totalWeight = 3, cyclicWeight = 1, maxAchievable = 2 >= quorum
|
||||
// No relaxation needed, bogie + demon satisfy quorum normally
|
||||
std::uint32_t aliceSeq = env.seq(alice);
|
||||
env(noop(alice),
|
||||
msig({msigner(becky, msigner(bogie), msigner(demon))}),
|
||||
L(),
|
||||
fee(5 * baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
|
||||
|
||||
// Should fail if only one non-cyclic signer provided
|
||||
aliceSeq = env.seq(alice);
|
||||
env(noop(alice),
|
||||
msig({msigner(becky, msigner(bogie))}),
|
||||
L(),
|
||||
fee(4 * baseFee),
|
||||
ter(tefBAD_QUORUM));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
||||
}
|
||||
|
||||
// Test Case 12: Partial cycle - one branch cyclic, one not
|
||||
{
|
||||
testcase("Cycle Detection - Partial Cycle");
|
||||
|
||||
// Reset signer lists
|
||||
env(signers(alice, jtx::none));
|
||||
env(signers(becky, jtx::none));
|
||||
env(signers(cheri, jtx::none));
|
||||
env.close();
|
||||
|
||||
// alice -> {becky, cheri}
|
||||
// becky -> {alice, bogie} (cyclic)
|
||||
// cheri -> {daria} (not cyclic)
|
||||
env(signers(alice, 2, {{becky, 1}, {cheri, 1}}));
|
||||
env(signers(becky, 1, {{alice, 1}, {bogie, 1}}));
|
||||
env(signers(cheri, 1, {{daria, 1}}));
|
||||
env.close();
|
||||
|
||||
// becky's branch has cycle, cheri's doesn't
|
||||
// Both contribute to alice's quorum
|
||||
std::uint32_t aliceSeq = env.seq(alice);
|
||||
env(noop(alice),
|
||||
msig(
|
||||
{msigner(becky, msigner(bogie)), // relaxed quorum
|
||||
msigner(cheri, msigner(daria))}), // normal quorum
|
||||
L(),
|
||||
fee(6 * baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
|
||||
}
|
||||
|
||||
// Test Case 13: Diamond pattern with cycle
|
||||
{
|
||||
testcase("Cycle Detection - Diamond Pattern");
|
||||
|
||||
// Reset signer lists
|
||||
env(signers(alice, jtx::none));
|
||||
env(signers(becky, jtx::none));
|
||||
env(signers(cheri, jtx::none));
|
||||
env(signers(daria, jtx::none));
|
||||
env.close();
|
||||
|
||||
// alice -> {becky, cheri}
|
||||
// becky -> {daria}
|
||||
// cheri -> {daria}
|
||||
// daria -> {alice, bogie} (cycle through both paths)
|
||||
env(signers(alice, 2, {{becky, 1}, {cheri, 1}}));
|
||||
env(signers(becky, 1, {{daria, 1}}));
|
||||
env(signers(cheri, 1, {{daria, 1}}));
|
||||
env(signers(daria, 1, {{alice, 1}, {bogie, 1}}));
|
||||
env.close();
|
||||
|
||||
// Both paths converge at daria, which cycles back to alice
|
||||
std::uint32_t aliceSeq = env.seq(alice);
|
||||
env(noop(alice),
|
||||
msig(
|
||||
{msigner(becky, msigner(daria, msigner(bogie))),
|
||||
msigner(cheri, msigner(daria, msigner(bogie)))}),
|
||||
L(),
|
||||
fee(7 * baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
|
||||
}
|
||||
|
||||
// Test Case 14: Cycle requiring maximum quorum relaxation
|
||||
{
|
||||
testcase("Cycle Detection - Maximum Relaxation");
|
||||
|
||||
Account const omega{"omega", KeyType::secp256k1};
|
||||
Account const sigma{"sigma", KeyType::ed25519};
|
||||
|
||||
env.fund(XRP(1000), omega, sigma);
|
||||
env.close();
|
||||
|
||||
// Reset alice and becky signer lists
|
||||
env(signers(alice, jtx::none));
|
||||
env(signers(becky, jtx::none));
|
||||
env.close();
|
||||
|
||||
// Set up signer lists FIRST
|
||||
env(signers(sigma, 1, {{omega, 1}, {bogie, 1}}));
|
||||
env(signers(omega, 3, {{sigma, 2}, {alice, 1}, {becky, 1}}));
|
||||
env(signers(alice, 1, {{omega, 1}, {demon, 1}}));
|
||||
env(signers(becky, 1, {{omega, 1}, {ghost, 1}}));
|
||||
env.close();
|
||||
|
||||
// NOW disable master keys
|
||||
env(fset(omega, asfDisableMaster), sig(omega));
|
||||
env(fset(sigma, asfDisableMaster), sig(sigma));
|
||||
env.close();
|
||||
|
||||
// From omega's perspective when signing for omega:
|
||||
// - sigma: needs omega (cyclic), so relaxes to bogie only
|
||||
// - alice: needs omega (cyclic), so relaxes to demon only
|
||||
// - becky: needs omega (cyclic), so relaxes to ghost only
|
||||
// All signers need relaxation but can be satisfied
|
||||
std::uint32_t omegaSeq = env.seq(omega);
|
||||
env(noop(omega),
|
||||
msig(
|
||||
{msigner(alice, msigner(demon)),
|
||||
msigner(becky, msigner(ghost)),
|
||||
msigner(sigma, msigner(bogie))}),
|
||||
L(),
|
||||
fee(7 * baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(omega) == omegaSeq + 1);
|
||||
}
|
||||
|
||||
// Test Case 15: Cycle at exact max depth boundary
|
||||
{
|
||||
testcase("Cycle Detection - Max Depth Boundary");
|
||||
|
||||
// Reset signer lists
|
||||
env(signers(alice, jtx::none));
|
||||
env(signers(becky, jtx::none));
|
||||
env(signers(cheri, jtx::none));
|
||||
env(signers(daria, jtx::none));
|
||||
env(signers(edgar, jtx::none));
|
||||
env.close();
|
||||
|
||||
// Depth 4 is max: alice(1) -> becky(2) -> cheri(3) -> daria(4)
|
||||
// daria cycles back but we're at max depth
|
||||
env(signers(alice, 1, {{becky, 1}}));
|
||||
env(signers(becky, 1, {{cheri, 1}}));
|
||||
env(signers(cheri, 1, {{daria, 1}}));
|
||||
env(signers(daria, 1, {{alice, 1}, {bogie, 1}}));
|
||||
env.close();
|
||||
|
||||
// This should work - cycle detected and relaxed at depth 4
|
||||
std::uint32_t aliceSeq = env.seq(alice);
|
||||
env(noop(alice),
|
||||
msig({msigner(
|
||||
becky, msigner(cheri, msigner(daria, msigner(bogie))))}),
|
||||
L(),
|
||||
fee(6 * baseFee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq + 1);
|
||||
|
||||
// Now try to exceed depth (add edgar at depth 5)
|
||||
env(signers(daria, 1, {{edgar, 1}}));
|
||||
env(signers(edgar, 1, {{bogie, 1}}));
|
||||
env.close();
|
||||
|
||||
// Transaction structure is rejected at preflight for exceeding
|
||||
// nesting limits
|
||||
aliceSeq = env.seq(alice);
|
||||
env(noop(alice),
|
||||
msig({msigner(
|
||||
becky,
|
||||
msigner(
|
||||
cheri,
|
||||
msigner(daria, msigner(edgar, msigner(bogie)))))}),
|
||||
L(),
|
||||
fee(7 * baseFee),
|
||||
ter(temMALFORMED)); // Rejected at preflight for excessive
|
||||
// nesting
|
||||
env.close();
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_signerListSetFlags(FeatureBitset features)
|
||||
{
|
||||
@@ -1709,6 +2503,7 @@ public:
|
||||
test_signForHash(features);
|
||||
test_signersWithTickets(features);
|
||||
test_signersWithTags(features);
|
||||
test_nestedMultiSign(features);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1721,8 +2516,11 @@ public:
|
||||
// featureMultiSignReserve. Limits on the number of signers
|
||||
// changes based on featureExpandedSignerList. Test both with and
|
||||
// without.
|
||||
testAll(all - featureMultiSignReserve - featureExpandedSignerList);
|
||||
testAll(all - featureExpandedSignerList);
|
||||
testAll(
|
||||
all - featureMultiSignReserve - featureExpandedSignerList -
|
||||
featureNestedMultiSign);
|
||||
testAll(all - featureExpandedSignerList - featureNestedMultiSign);
|
||||
testAll(all - featureNestedMultiSign);
|
||||
testAll(all);
|
||||
|
||||
test_signerListSetFlags(all);
|
||||
|
||||
@@ -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(
|
||||
signers.begin(),
|
||||
signers.end(),
|
||||
[](msig::Reg const& lhs, msig::Reg const& rhs) {
|
||||
return lhs.acct.id() < rhs.acct.id();
|
||||
[](msig::SignerPtr const& lhs, msig::SignerPtr const& rhs) {
|
||||
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
|
||||
@@ -93,19 +123,47 @@ msig::operator()(Env& env, JTx& jt) const
|
||||
env.test.log << pretty(jtx.jv) << std::endl;
|
||||
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()];
|
||||
for (std::size_t i = 0; i < mySigners.size(); ++i)
|
||||
{
|
||||
auto const& e = mySigners[i];
|
||||
auto& jo = js[i][sfSigner.getJsonName()];
|
||||
jo[jss::Account] = e.acct.human();
|
||||
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()});
|
||||
jo = buildSignerJson(mySigners[i]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#define RIPPLE_TEST_JTX_MULTISIGN_H_INCLUDED
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/amount.h>
|
||||
@@ -65,6 +66,48 @@ signers(Account const& account, none_t);
|
||||
class msig
|
||||
{
|
||||
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
|
||||
{
|
||||
Account acct;
|
||||
@@ -73,16 +116,13 @@ public:
|
||||
Reg(Account const& masterSig) : acct(masterSig), sig(masterSig)
|
||||
{
|
||||
}
|
||||
|
||||
Reg(Account const& acct_, Account const& regularSig)
|
||||
: acct(acct_), sig(regularSig)
|
||||
{
|
||||
}
|
||||
|
||||
Reg(char const* masterSig) : acct(masterSig), sig(masterSig)
|
||||
{
|
||||
}
|
||||
|
||||
Reg(char const* acct_, char const* regularSig)
|
||||
: acct(acct_), sig(regularSig)
|
||||
{
|
||||
@@ -93,13 +133,32 @@ public:
|
||||
{
|
||||
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:
|
||||
// Initializer list constructor - resolves brace-init ambiguity
|
||||
msig(std::initializer_list<SignerPtr> signers_)
|
||||
: msig(std::vector<SignerPtr>(signers_))
|
||||
{
|
||||
// handled by :
|
||||
}
|
||||
|
||||
// Direct constructor with SignerPtr vector
|
||||
explicit msig(std::vector<SignerPtr> signers_);
|
||||
|
||||
// Backward compatibility constructor
|
||||
msig(std::vector<Reg> signers_);
|
||||
|
||||
// Variadic constructor for backward compatibility
|
||||
template <class AccountType, class... Accounts>
|
||||
explicit msig(AccountType&& a0, Accounts&&... aN)
|
||||
: msig{std::vector<Reg>{
|
||||
@@ -112,6 +171,30 @@ public:
|
||||
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. */
|
||||
|
||||
@@ -1757,30 +1757,32 @@ public:
|
||||
|
||||
// This lambda contains the bulk of the test code.
|
||||
auto testMalformedSigningAccount =
|
||||
[this, &txn](STObject const& signer, bool expectPass) {
|
||||
// Create SigningAccounts array.
|
||||
STArray signers(sfSigners, 1);
|
||||
signers.push_back(signer);
|
||||
[this, &txn](
|
||||
STObject const& signer, bool expectPass) -> bool /* passed */ {
|
||||
// Create SigningAccounts array.
|
||||
STArray signers(sfSigners, 1);
|
||||
signers.push_back(signer);
|
||||
|
||||
// Insert signers into transaction.
|
||||
STTx tempTxn(txn);
|
||||
tempTxn.setFieldArray(sfSigners, signers);
|
||||
// Insert signers into transaction.
|
||||
STTx tempTxn(txn);
|
||||
tempTxn.setFieldArray(sfSigners, signers);
|
||||
|
||||
Serializer rawTxn;
|
||||
tempTxn.add(rawTxn);
|
||||
SerialIter sit(rawTxn.slice());
|
||||
bool serialized = false;
|
||||
try
|
||||
{
|
||||
STTx copy(sit);
|
||||
serialized = true;
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
; // If it threw then serialization failed.
|
||||
}
|
||||
BEAST_EXPECT(serialized == expectPass);
|
||||
};
|
||||
Serializer rawTxn;
|
||||
tempTxn.add(rawTxn);
|
||||
SerialIter sit(rawTxn.slice());
|
||||
bool serialized = false;
|
||||
try
|
||||
{
|
||||
STTx copy(sit);
|
||||
serialized = true;
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
; // If it threw then serialization failed.
|
||||
}
|
||||
BEAST_EXPECT(serialized == expectPass);
|
||||
return serialized == expectPass;
|
||||
};
|
||||
|
||||
{
|
||||
// Test case 1. Make a valid Signer object.
|
||||
@@ -1790,13 +1792,15 @@ public:
|
||||
soTest1.setFieldVL(sfTxnSignature, saMultiSignature);
|
||||
testMalformedSigningAccount(soTest1, true);
|
||||
}
|
||||
{
|
||||
|
||||
/*{ // RHNOTE: featureNestedMultiSign covers this in the
|
||||
checkMultiSign()
|
||||
// Test case 2. Omit sfSigningPubKey from SigningAccount.
|
||||
STObject soTest2(sfSigner);
|
||||
soTest2.setAccountID(sfAccount, id2);
|
||||
soTest2.setFieldVL(sfTxnSignature, saMultiSignature);
|
||||
testMalformedSigningAccount(soTest2, false);
|
||||
}
|
||||
}*/
|
||||
{
|
||||
// Test case 3. Extra sfAmount in SigningAccount.
|
||||
STObject soTest3(sfSigner);
|
||||
|
||||
@@ -332,6 +332,7 @@ multi_runner_child::run_multi(Pred pred)
|
||||
{
|
||||
if (!pred(*t))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
failed = run(*t) || failed;
|
||||
|
||||
Reference in New Issue
Block a user