mirror of
https://github.com/Xahau/xahaud.git
synced 2026-04-29 15:37:46 +00:00
Compare commits
15 Commits
cli-defini
...
multi-sig-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e27eb4f12d | ||
|
|
37bfa0af49 | ||
|
|
69b82beafa | ||
|
|
9d4e507675 | ||
|
|
49fd0c33b5 | ||
|
|
0ffb6e8c21 | ||
|
|
61138058a6 | ||
|
|
8dd17973d0 | ||
|
|
198ff08ee5 | ||
|
|
d6aad6744e | ||
|
|
dd9e6053a0 | ||
|
|
339f1b7f6f | ||
|
|
bb6553193e | ||
|
|
ffcfd94327 | ||
|
|
1baeb9fb33 |
2
.github/workflows/clang-format.yml
vendored
2
.github/workflows/clang-format.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install clang-format-${CLANG_VERSION}
|
||||
- name: Format first-party sources
|
||||
run: find include src -type f \( -name '*.cpp' -o -name '*.hpp' -o -name '*.h' -o -name '*.ipp' \) -not -path "src/magic/magic_enum.h" -exec clang-format-${CLANG_VERSION} -i {} +
|
||||
run: find include src -type f \( -name '*.cpp' -o -name '*.hpp' -o -name '*.h' -o -name '*.ipp' \) -exec clang-format-${CLANG_VERSION} -i {} +
|
||||
- name: Check for differences
|
||||
id: assert
|
||||
run: |
|
||||
|
||||
@@ -18,6 +18,10 @@ jobs:
|
||||
generator: bash ./hook/generate_sfcodes.sh
|
||||
- target: hook/tts.h
|
||||
generator: ./hook/generate_tts.sh
|
||||
- target: hook/ls_flags.h
|
||||
generator: ./hook/generate_lsflags.sh
|
||||
- target: hook/tx_flags.h
|
||||
generator: ./hook/generate_txflags.sh
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
CLANG_VERSION: 18
|
||||
|
||||
13
.github/workflows/xahau-ga-nix.yml
vendored
13
.github/workflows/xahau-ga-nix.yml
vendored
@@ -464,16 +464,3 @@ jobs:
|
||||
verbose: true
|
||||
plugins: noop
|
||||
use_oidc: true
|
||||
|
||||
- name: Export server definitions
|
||||
if: matrix.job_type == 'build' && matrix.compiler_id == 'gcc-13-libstdcxx'
|
||||
run: |
|
||||
${{ env.build_dir }}/rippled --definitions | python3 -m json.tool > server_definitions.json
|
||||
|
||||
- name: Upload server definitions
|
||||
if: matrix.job_type == 'build' && matrix.compiler_id == 'gcc-13-libstdcxx'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: server-definitions
|
||||
path: server_definitions.json
|
||||
archive: false
|
||||
|
||||
@@ -122,6 +122,7 @@ endif()
|
||||
find_package(nudb REQUIRED)
|
||||
find_package(date REQUIRED)
|
||||
find_package(xxHash REQUIRED)
|
||||
find_package(magic_enum REQUIRED)
|
||||
|
||||
include(deps/WasmEdge)
|
||||
if(TARGET nudb::core)
|
||||
|
||||
@@ -380,6 +380,7 @@ function(setup_target_for_coverage_gcovr)
|
||||
${GCOVR_PATH}
|
||||
--gcov-executable ${GCOV_TOOL}
|
||||
--gcov-ignore-parse-errors=negative_hits.warn_once_per_file
|
||||
--gcov-ignore-parse-errors=suspicious_hits.warn_once_per_file
|
||||
-r ${BASEDIR}
|
||||
${GCOVR_ADDITIONAL_ARGS}
|
||||
${GCOVR_EXCLUDE_ARGS}
|
||||
|
||||
@@ -54,6 +54,7 @@ add_library(xrpl.imports.main INTERFACE)
|
||||
target_link_libraries(xrpl.imports.main
|
||||
INTERFACE
|
||||
LibArchive::LibArchive
|
||||
magic_enum::magic_enum
|
||||
OpenSSL::Crypto
|
||||
Ripple::boost
|
||||
wasmedge::wasmedge
|
||||
|
||||
@@ -29,6 +29,7 @@ class Xrpl(ConanFile):
|
||||
'date/3.0.3',
|
||||
'grpc/1.50.1',
|
||||
'libarchive/3.7.6',
|
||||
'magic_enum/0.9.5',
|
||||
'nudb/2.0.8',
|
||||
'openssl/3.6.0',
|
||||
'soci/4.0.3@xahaud/stable',
|
||||
|
||||
82
hook/generate_lsflags.sh
Executable file
82
hook/generate_lsflags.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
SCRIPT_DIR=$(cd "$SCRIPT_DIR" && pwd)
|
||||
|
||||
RIPPLED_ROOT="$SCRIPT_DIR/../include/xrpl"
|
||||
LEDGER_FORMATS="$RIPPLED_ROOT/protocol/LedgerFormats.h"
|
||||
|
||||
echo '// Generated using generate_lsflags.sh'
|
||||
echo ''
|
||||
echo '#ifndef HOOKLSFLAGS_INCLUDED'
|
||||
echo '#define HOOKLSFLAGS_INCLUDED 1'
|
||||
echo ''
|
||||
awk '
|
||||
function ltrim(s) { sub(/^[[:space:]]+/, "", s); return s }
|
||||
function rtrim(s) { sub(/[[:space:]]+$/, "", s); return s }
|
||||
function trim(s) { return rtrim(ltrim(s)) }
|
||||
|
||||
function flush_group() {
|
||||
if (entry_count > 0 && group != "") {
|
||||
printf "enum %s {\n", group
|
||||
for (i = 1; i <= entry_count; i++) {
|
||||
printf " %s,\n", entries[i]
|
||||
}
|
||||
printf "};\n"
|
||||
}
|
||||
delete entries
|
||||
entry_count = 0
|
||||
}
|
||||
|
||||
/enum LedgerSpecificFlags \{/ { inside = 1; next }
|
||||
inside && /^\};/ { inside = 0; flush_group(); next }
|
||||
!inside { next }
|
||||
|
||||
# Group header comments: // ltFOO or // remarks
|
||||
/^[[:space:]]*\/\/[[:space:]]*(lt[A-Z_]+|remarks)[[:space:]]*$/ {
|
||||
flush_group()
|
||||
line = $0
|
||||
sub(/.*\/\/[[:space:]]*/, "", line)
|
||||
group = trim(line)
|
||||
next
|
||||
}
|
||||
|
||||
# Skip pure comment lines (not group headers)
|
||||
/^[[:space:]]*\/\// { next }
|
||||
|
||||
# Skip blank lines
|
||||
/^[[:space:]]*$/ { next }
|
||||
|
||||
# Accumulate flag lines (handle multi-line values)
|
||||
{
|
||||
line = $0
|
||||
# Strip inline comments
|
||||
sub(/\/\/.*/, "", line)
|
||||
line = trim(line)
|
||||
if (line == "") next
|
||||
|
||||
if (pending != "") {
|
||||
pending = pending " " line
|
||||
} else {
|
||||
pending = line
|
||||
}
|
||||
|
||||
# If line ends with comma, the entry is complete
|
||||
if (pending ~ /,$/) {
|
||||
# Remove trailing comma
|
||||
sub(/,$/, "", pending)
|
||||
entries[++entry_count] = pending
|
||||
pending = ""
|
||||
}
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
inside = 0
|
||||
group = ""
|
||||
pending = ""
|
||||
entry_count = 0
|
||||
}
|
||||
' "$LEDGER_FORMATS"
|
||||
echo ''
|
||||
echo '#endif // HOOKLSFLAGS_INCLUDED'
|
||||
25
hook/generate_txflags.sh
Executable file
25
hook/generate_txflags.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
SCRIPT_DIR=$(cd "$SCRIPT_DIR" && pwd)
|
||||
|
||||
RIPPLED_ROOT="$SCRIPT_DIR/../include/xrpl"
|
||||
TX_FLAGS="$RIPPLED_ROOT/protocol/TxFlags.h"
|
||||
|
||||
echo '// Generated using generate_txflags.sh'
|
||||
echo '#include "ls_flags.h"'
|
||||
echo '#include <stdint.h>'
|
||||
echo ''
|
||||
cat "$TX_FLAGS" |
|
||||
awk '
|
||||
/^[[:space:]]*enum / {
|
||||
if (count > 0) print ""
|
||||
inside = 1
|
||||
count++
|
||||
}
|
||||
inside {
|
||||
print
|
||||
if (/};/) inside = 0
|
||||
}
|
||||
'
|
||||
@@ -49,4 +49,7 @@
|
||||
#include "macro.h"
|
||||
#include "tts.h"
|
||||
|
||||
#include "ls_flags.h"
|
||||
#include "tx_flags.h"
|
||||
|
||||
#endif
|
||||
|
||||
75
hook/ls_flags.h
Normal file
75
hook/ls_flags.h
Normal file
@@ -0,0 +1,75 @@
|
||||
// Generated using generate_lsflags.sh
|
||||
|
||||
#ifndef HOOKLSFLAGS_INCLUDED
|
||||
#define HOOKLSFLAGS_INCLUDED 1
|
||||
|
||||
enum ltACCOUNT_ROOT {
|
||||
lsfPasswordSpent = 0x00010000,
|
||||
lsfRequireDestTag = 0x00020000,
|
||||
lsfRequireAuth = 0x00040000,
|
||||
lsfDisallowXRP = 0x00080000,
|
||||
lsfDisableMaster = 0x00100000,
|
||||
lsfNoFreeze = 0x00200000,
|
||||
lsfGlobalFreeze = 0x00400000,
|
||||
lsfDefaultRipple = 0x00800000,
|
||||
lsfDepositAuth = 0x01000000,
|
||||
lsfTshCollect = 0x02000000,
|
||||
lsfDisallowIncomingNFTokenOffer = 0x04000000,
|
||||
lsfDisallowIncomingCheck = 0x08000000,
|
||||
lsfDisallowIncomingPayChan = 0x10000000,
|
||||
lsfDisallowIncomingTrustline = 0x20000000,
|
||||
lsfURITokenIssuer = 0x40000000,
|
||||
lsfDisallowIncomingRemit = 0x80000000,
|
||||
lsfAllowTrustLineClawback = 0x00001000,
|
||||
};
|
||||
enum ltOFFER {
|
||||
lsfPassive = 0x00010000,
|
||||
lsfSell = 0x00020000,
|
||||
};
|
||||
enum ltRIPPLE_STATE {
|
||||
lsfLowReserve = 0x00010000,
|
||||
lsfHighReserve = 0x00020000,
|
||||
lsfLowAuth = 0x00040000,
|
||||
lsfHighAuth = 0x00080000,
|
||||
lsfLowNoRipple = 0x00100000,
|
||||
lsfHighNoRipple = 0x00200000,
|
||||
lsfLowFreeze = 0x00400000,
|
||||
lsfHighFreeze = 0x00800000,
|
||||
lsfLowDeepFreeze = 0x02000000,
|
||||
lsfHighDeepFreeze = 0x04000000,
|
||||
lsfAMMNode = 0x01000000,
|
||||
};
|
||||
enum ltSIGNER_LIST {
|
||||
lsfOneOwnerCount = 0x00010000,
|
||||
};
|
||||
enum ltDIR_NODE {
|
||||
lsfNFTokenBuyOffers = 0x00000001,
|
||||
lsfNFTokenSellOffers = 0x00000002,
|
||||
lsfEmittedDir = 0x00000004,
|
||||
};
|
||||
enum ltNFTOKEN_OFFER {
|
||||
lsfSellNFToken = 0x00000001,
|
||||
};
|
||||
enum ltURI_TOKEN {
|
||||
lsfBurnable = 0x00000001,
|
||||
};
|
||||
enum remarks {
|
||||
lsfImmutable = 1,
|
||||
};
|
||||
enum ltMPTOKEN_ISSUANCE {
|
||||
lsfMPTLocked = 0x00000001,
|
||||
lsfMPTCanLock = 0x00000002,
|
||||
lsfMPTRequireAuth = 0x00000004,
|
||||
lsfMPTCanEscrow = 0x00000008,
|
||||
lsfMPTCanTrade = 0x00000010,
|
||||
lsfMPTCanTransfer = 0x00000020,
|
||||
lsfMPTCanClawback = 0x00000040,
|
||||
};
|
||||
enum ltMPTOKEN {
|
||||
lsfMPTAuthorized = 0x00000002,
|
||||
};
|
||||
enum ltCREDENTIAL {
|
||||
lsfAccepted = 0x00010000,
|
||||
};
|
||||
|
||||
#endif // HOOKLSFLAGS_INCLUDED
|
||||
@@ -274,7 +274,6 @@
|
||||
#define sfDisabledValidator ((14U << 16U) + 19U)
|
||||
#define sfEmittedTxn ((14U << 16U) + 20U)
|
||||
#define sfHookExecution ((14U << 16U) + 21U)
|
||||
#define sfHookDefinition ((14U << 16U) + 22U)
|
||||
#define sfHookParameter ((14U << 16U) + 23U)
|
||||
#define sfHookGrant ((14U << 16U) + 24U)
|
||||
#define sfVoteEntry ((14U << 16U) + 25U)
|
||||
|
||||
117
hook/tx_flags.h
Normal file
117
hook/tx_flags.h
Normal file
@@ -0,0 +1,117 @@
|
||||
// Generated using generate_txflags.sh
|
||||
#include "ls_flags.h"
|
||||
#include <stdint.h>
|
||||
|
||||
enum UniversalFlags : uint32_t {
|
||||
tfFullyCanonicalSig = 0x80000000,
|
||||
};
|
||||
|
||||
enum AccountSetFlags : uint32_t {
|
||||
tfRequireDestTag = 0x00010000,
|
||||
tfOptionalDestTag = 0x00020000,
|
||||
tfRequireAuth = 0x00040000,
|
||||
tfOptionalAuth = 0x00080000,
|
||||
tfDisallowXRP = 0x00100000,
|
||||
tfAllowXRP = 0x00200000,
|
||||
};
|
||||
|
||||
enum AccountFlags : uint32_t {
|
||||
asfRequireDest = 1,
|
||||
asfRequireAuth = 2,
|
||||
asfDisallowXRP = 3,
|
||||
asfDisableMaster = 4,
|
||||
asfAccountTxnID = 5,
|
||||
asfNoFreeze = 6,
|
||||
asfGlobalFreeze = 7,
|
||||
asfDefaultRipple = 8,
|
||||
asfDepositAuth = 9,
|
||||
asfAuthorizedNFTokenMinter = 10,
|
||||
asfTshCollect = 11,
|
||||
asfDisallowIncomingNFTokenOffer = 12,
|
||||
asfDisallowIncomingCheck = 13,
|
||||
asfDisallowIncomingPayChan = 14,
|
||||
asfDisallowIncomingTrustline = 15,
|
||||
asfDisallowIncomingRemit = 16,
|
||||
asfAllowTrustLineClawback = 17,
|
||||
};
|
||||
|
||||
enum OfferCreateFlags : uint32_t {
|
||||
tfPassive = 0x00010000,
|
||||
tfImmediateOrCancel = 0x00020000,
|
||||
tfFillOrKill = 0x00040000,
|
||||
tfSell = 0x00080000,
|
||||
};
|
||||
|
||||
enum PaymentFlags : uint32_t {
|
||||
tfNoRippleDirect = 0x00010000,
|
||||
tfPartialPayment = 0x00020000,
|
||||
tfLimitQuality = 0x00040000,
|
||||
};
|
||||
|
||||
enum TrustSetFlags : uint32_t {
|
||||
tfSetfAuth = 0x00010000,
|
||||
tfSetNoRipple = 0x00020000,
|
||||
tfClearNoRipple = 0x00040000,
|
||||
tfSetFreeze = 0x00100000,
|
||||
tfClearFreeze = 0x00200000,
|
||||
tfSetDeepFreeze = 0x00400000,
|
||||
tfClearDeepFreeze = 0x00800000
|
||||
};
|
||||
|
||||
enum EnableAmendmentFlags : uint32_t {
|
||||
tfGotMajority = 0x00010000,
|
||||
tfLostMajority = 0x00020000,
|
||||
tfTestSuite = 0x80000000,
|
||||
};
|
||||
|
||||
enum PaymentChannelClaimFlags : uint32_t {
|
||||
tfRenew = 0x00010000,
|
||||
tfClose = 0x00020000,
|
||||
};
|
||||
|
||||
enum NFTokenMintFlags : uint32_t {
|
||||
tfBurnable = 0x00000001,
|
||||
tfOnlyXRP = 0x00000002,
|
||||
tfTrustLine = 0x00000004,
|
||||
tfTransferable = 0x00000008,
|
||||
tfMutable = 0x00000010,
|
||||
tfStrongTSH = 0x00008000,
|
||||
};
|
||||
|
||||
enum MPTokenIssuanceCreateFlags : uint32_t {
|
||||
tfMPTCanLock = lsfMPTCanLock,
|
||||
tfMPTRequireAuth = lsfMPTRequireAuth,
|
||||
tfMPTCanEscrow = lsfMPTCanEscrow,
|
||||
tfMPTCanTrade = lsfMPTCanTrade,
|
||||
tfMPTCanTransfer = lsfMPTCanTransfer,
|
||||
tfMPTCanClawback = lsfMPTCanClawback,
|
||||
};
|
||||
|
||||
enum MPTokenAuthorizeFlags : uint32_t {
|
||||
tfMPTUnauthorize = 0x00000001,
|
||||
};
|
||||
|
||||
enum MPTokenIssuanceSetFlags : uint32_t {
|
||||
tfMPTLock = 0x00000001,
|
||||
tfMPTUnlock = 0x00000002,
|
||||
};
|
||||
|
||||
enum NFTokenCreateOfferFlags : uint32_t {
|
||||
tfSellNFToken = 0x00000001,
|
||||
};
|
||||
|
||||
enum ClaimRewardFlags : uint32_t {
|
||||
tfOptOut = 0x00000001,
|
||||
};
|
||||
|
||||
enum CronSetFlags : uint32_t {
|
||||
tfCronUnset = 0x00000001,
|
||||
};
|
||||
|
||||
enum AMMClawbackFlags : uint32_t {
|
||||
tfClawTwoAssets = 0x00000001,
|
||||
};
|
||||
|
||||
enum BridgeModifyFlags : uint32_t {
|
||||
tfClearAccountCreateAmount = 0x00010000,
|
||||
};
|
||||
@@ -80,7 +80,7 @@ namespace detail {
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
|
||||
// the actual number of amendments. A LogicError on startup will verify this.
|
||||
static constexpr std::size_t numFeatures = 114;
|
||||
static constexpr std::size_t numFeatures = 116;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
|
||||
@@ -195,6 +195,71 @@ STTx::getTransactionID() const
|
||||
return tid_;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Multi-sign depth and leaf limits
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Maximum nesting depth for nested multi-signing (featureNestedMultiSign). */
|
||||
constexpr int nestedMultiSignMaxDepth = 4;
|
||||
|
||||
/** Maximum nesting depth when nested multi-signing is disabled (flat only). */
|
||||
constexpr int legacyMultiSignMaxDepth = 1;
|
||||
|
||||
/** Maximum total leaf signers across the entire nested tree.
|
||||
Bounds worst-case signature verification cost. Only enforced when
|
||||
featureNestedMultiSign is enabled; flat signing is already capped by the
|
||||
per-array multisign limit.
|
||||
*/
|
||||
constexpr std::size_t nestedMultiSignMaxLeafSigners = 64;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Multi-sign signer entry helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Count populated fields in an STObject.
|
||||
STObject::getCount() includes template slots for optional fields that are
|
||||
not present. Signer shape validation needs only populated fields.
|
||||
*/
|
||||
inline std::size_t
|
||||
countPresentFields(STObject const& obj)
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (auto const& field : obj)
|
||||
{
|
||||
if (field.getSType() != STI_NOTPRESENT)
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** A leaf signer has Account + SigningPubKey + TxnSignature only. */
|
||||
inline bool
|
||||
isLeafSigner(STObject const& signer)
|
||||
{
|
||||
return signer.isFieldPresent(sfAccount) &&
|
||||
signer.isFieldPresent(sfSigningPubKey) &&
|
||||
signer.isFieldPresent(sfTxnSignature) &&
|
||||
!signer.isFieldPresent(sfSigners) && countPresentFields(signer) == 3;
|
||||
}
|
||||
|
||||
/** A nested signer has Account + Signers only. */
|
||||
inline bool
|
||||
isNestedSigner(STObject const& signer)
|
||||
{
|
||||
return signer.isFieldPresent(sfAccount) &&
|
||||
signer.isFieldPresent(sfSigners) &&
|
||||
!signer.isFieldPresent(sfSigningPubKey) &&
|
||||
!signer.isFieldPresent(sfTxnSignature) &&
|
||||
countPresentFields(signer) == 2;
|
||||
}
|
||||
|
||||
/** True when a signer entry is either a valid leaf or a valid nested signer. */
|
||||
inline bool
|
||||
isValidSignerEntry(STObject const& signer)
|
||||
{
|
||||
return isLeafSigner(signer) || isNestedSigner(signer);
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
@@ -132,7 +132,7 @@ constexpr std::uint32_t tfTrustSetMask =
|
||||
tfClearFreeze | tfSetDeepFreeze | tfClearDeepFreeze);
|
||||
|
||||
// EnableAmendment flags:
|
||||
enum EnableAmendmentFlags : std::uint32_t {
|
||||
enum EnableAmendmentFlags : uint32_t {
|
||||
tfGotMajority = 0x00010000,
|
||||
tfLostMajority = 0x00020000,
|
||||
tfTestSuite = 0x80000000,
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(NestedMultiSign, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (IOULockedBalanceInvariant, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (ImportIssuer, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(HookAPISerializedType240, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -367,7 +367,7 @@ UNTYPED_SFIELD(sfMajority, OBJECT, 18)
|
||||
UNTYPED_SFIELD(sfDisabledValidator, OBJECT, 19)
|
||||
UNTYPED_SFIELD(sfEmittedTxn, OBJECT, 20)
|
||||
UNTYPED_SFIELD(sfHookExecution, OBJECT, 21)
|
||||
UNTYPED_SFIELD(sfHookDefinition, OBJECT, 22)
|
||||
// 22 unused
|
||||
UNTYPED_SFIELD(sfHookParameter, OBJECT, 23)
|
||||
UNTYPED_SFIELD(sfHookGrant, OBJECT, 24)
|
||||
UNTYPED_SFIELD(sfVoteEntry, OBJECT, 25)
|
||||
|
||||
@@ -49,8 +49,9 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
sfSigner.getCode(),
|
||||
{
|
||||
{sfAccount, soeREQUIRED},
|
||||
{sfSigningPubKey, soeREQUIRED},
|
||||
{sfTxnSignature, soeREQUIRED},
|
||||
{sfSigningPubKey, soeOPTIONAL},
|
||||
{sfTxnSignature, soeOPTIONAL},
|
||||
{sfSigners, soeOPTIONAL},
|
||||
});
|
||||
|
||||
add(sfMajority.jsonName,
|
||||
@@ -87,19 +88,6 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfEmittedTxnID, soeREQUIRED},
|
||||
{sfEmitNonce, soeOPTIONAL}});
|
||||
|
||||
add(sfHookDefinition.jsonName,
|
||||
sfHookDefinition.getCode(),
|
||||
{{sfCreateCode, soeREQUIRED},
|
||||
{sfHookNamespace, soeREQUIRED},
|
||||
{sfHookParameters, soeREQUIRED},
|
||||
{sfHookOn, soeOPTIONAL},
|
||||
{sfHookOnIncoming, soeOPTIONAL},
|
||||
{sfHookOnOutgoing, soeOPTIONAL},
|
||||
{sfHookCanEmit, soeOPTIONAL},
|
||||
{sfHookApiVersion, soeREQUIRED},
|
||||
{sfFlags, soeREQUIRED},
|
||||
{sfFee, soeREQUIRED}});
|
||||
|
||||
add(sfHook.jsonName,
|
||||
sfHook.getCode(),
|
||||
{{sfHookHash, soeOPTIONAL},
|
||||
|
||||
@@ -370,11 +370,6 @@ STTx::checkMultiSign(
|
||||
|
||||
STArray const& signers{getFieldArray(sfSigners)};
|
||||
|
||||
// There are well known bounds that the number of signers must be within.
|
||||
if (signers.size() < minMultiSigners ||
|
||||
signers.size() > maxMultiSigners(&rules))
|
||||
return Unexpected("Invalid Signers array size.");
|
||||
|
||||
// We can ease the computational load inside the loop a bit by
|
||||
// pre-constructing part of the data that we hash. Fill a Serializer
|
||||
// with the stuff that stays constant from signature to signature.
|
||||
@@ -387,64 +382,117 @@ 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 and leaf cap based on feature flag.
|
||||
bool const nested = rules.enabled(featureNestedMultiSign);
|
||||
int const maxDepth =
|
||||
nested ? nestedMultiSignMaxDepth : legacyMultiSignMaxDepth;
|
||||
std::size_t const maxLeafSigners =
|
||||
nested ? nestedMultiSignMaxLeafSigners : maxMultiSigners(&rules);
|
||||
std::size_t totalLeafSigners = 0;
|
||||
|
||||
// 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,
|
||||
[[maybe_unused]] 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;
|
||||
|
||||
if (isNestedSigner(signer))
|
||||
{
|
||||
Blob const signature = signer.getFieldVL(sfTxnSignature);
|
||||
if (!nested)
|
||||
return Unexpected("FeatureNestedMultiSign is disabled");
|
||||
|
||||
// wildcard network gets a free pass
|
||||
validSig = isWildcardNetwork ||
|
||||
verify(PublicKey(makeSlice(spk)),
|
||||
s.slice(),
|
||||
makeSlice(signature),
|
||||
fullyCanonical);
|
||||
STArray const& nestedSigners = signer.getFieldArray(sfSigners);
|
||||
auto result =
|
||||
checkSignersArray(nestedSigners, accountID, depth + 1);
|
||||
if (!result)
|
||||
return result;
|
||||
}
|
||||
else if (isLeafSigner(signer))
|
||||
{
|
||||
if (++totalLeafSigners > maxLeafSigners)
|
||||
return Unexpected(std::string("Too many leaf signers."));
|
||||
|
||||
bool validSig = false;
|
||||
try
|
||||
{
|
||||
Serializer s = dataStart;
|
||||
finishMultiSigningData(accountID, s);
|
||||
|
||||
auto spk = signer.getFieldVL(sfSigningPubKey);
|
||||
|
||||
if (publicKeyType(makeSlice(spk)))
|
||||
{
|
||||
Blob const signature =
|
||||
signer.getFieldVL(sfTxnSignature);
|
||||
|
||||
// wildcard network gets a free pass
|
||||
validSig = isWildcardNetwork ||
|
||||
verify(PublicKey(makeSlice(spk)),
|
||||
s.slice(),
|
||||
makeSlice(signature),
|
||||
fullyCanonical);
|
||||
}
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
// We assume any problem lies with the signature.
|
||||
validSig = false;
|
||||
}
|
||||
if (!validSig)
|
||||
return Unexpected(
|
||||
std::string("Invalid signature on account ") +
|
||||
toBase58(accountID) + ".");
|
||||
}
|
||||
else
|
||||
{
|
||||
return Unexpected(
|
||||
std::string("Malformed signer entry for account ") +
|
||||
toBase58(accountID) + ".");
|
||||
}
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,8 @@
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <xrpld/app/paths/AccountCurrencies.h>
|
||||
#include <xrpld/app/paths/Pathfinder.h>
|
||||
#include <xrpld/app/paths/RippleLineCache.h>
|
||||
#include <xrpld/core/JobQueue.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/RPCHandler.h>
|
||||
@@ -573,6 +575,164 @@ public:
|
||||
BEAST_EXPECT(equal(sa, Account("alice")["USD"](5)));
|
||||
}
|
||||
|
||||
Json::Value
|
||||
six_path_append_request_result()
|
||||
{
|
||||
using namespace jtx;
|
||||
Env env = pathTestEnv();
|
||||
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
Account G1{"G1"};
|
||||
Account G2{"G2"};
|
||||
Account M1{"M1"};
|
||||
Account M2{"M2"};
|
||||
Account M3{"M3"};
|
||||
Account M4{"M4"};
|
||||
Account M5{"M5"};
|
||||
Account M6{"M6"};
|
||||
Account MM{"MM"};
|
||||
|
||||
env.fund(XRP(1000), A1, A2, G1, G2, M1, M2, M3, M4, M5, M6, MM);
|
||||
env.close();
|
||||
|
||||
env.trust(G1["HKD"](2000), A1);
|
||||
env.trust(G2["HKD"](2000), A2);
|
||||
|
||||
env.trust(G1["HKD"](100000), M1, M2, M3, M4, M5, M6, MM);
|
||||
env.trust(G2["HKD"](100000), M1, M2, M3, M4, M5, M6, MM);
|
||||
env.close();
|
||||
|
||||
env(pay(G1, A1, G1["HKD"](1000)));
|
||||
|
||||
env(pay(G1, M1, G1["HKD"](10)));
|
||||
env(pay(G1, M2, G1["HKD"](10)));
|
||||
env(pay(G1, M3, G1["HKD"](10)));
|
||||
env(pay(G1, M4, G1["HKD"](10)));
|
||||
env(pay(G1, M5, G1["HKD"](10)));
|
||||
env(pay(G1, M6, G1["HKD"](10)));
|
||||
env(pay(G1, MM, G1["HKD"](1000)));
|
||||
|
||||
env(pay(G2, M1, G2["HKD"](10)));
|
||||
env(pay(G2, M2, G2["HKD"](10)));
|
||||
env(pay(G2, M3, G2["HKD"](10)));
|
||||
env(pay(G2, M4, G2["HKD"](10)));
|
||||
env(pay(G2, M5, G2["HKD"](10)));
|
||||
env(pay(G2, M6, G2["HKD"](10)));
|
||||
env(pay(G2, MM, G2["HKD"](1000)));
|
||||
env.close();
|
||||
|
||||
env(offer(MM, G1["HKD"](1000), G2["HKD"](100)));
|
||||
env.close();
|
||||
|
||||
return find_paths_request(
|
||||
env, A1, A2, A2["HKD"](60), std::nullopt, G1["HKD"].currency);
|
||||
}
|
||||
|
||||
void
|
||||
pathfind_paths_computed_never_exceeds_six()
|
||||
{
|
||||
testcase("pathfind paths_computed never exceeds six");
|
||||
|
||||
auto const result = six_path_append_request_result();
|
||||
BEAST_EXPECT(result.isMember(jss::alternatives));
|
||||
if (!result.isMember(jss::alternatives))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(result[jss::alternatives].isArray());
|
||||
if (!result[jss::alternatives].isArray())
|
||||
return;
|
||||
|
||||
bool sawPathsComputed = false;
|
||||
for (auto const& alt : result[jss::alternatives])
|
||||
{
|
||||
if (!alt.isMember(jss::paths_computed))
|
||||
continue;
|
||||
sawPathsComputed = true;
|
||||
BEAST_EXPECT(alt[jss::paths_computed].isArray());
|
||||
if (alt[jss::paths_computed].isArray())
|
||||
BEAST_EXPECT(alt[jss::paths_computed].size() <= 6);
|
||||
}
|
||||
BEAST_EXPECT(sawPathsComputed);
|
||||
}
|
||||
|
||||
void
|
||||
pathfind_can_return_six_paths_with_append()
|
||||
{
|
||||
testcase("pathfind can return six paths with append");
|
||||
using namespace jtx;
|
||||
Env env = pathTestEnv();
|
||||
|
||||
Account A1{"A1"};
|
||||
Account A2{"A2"};
|
||||
Account G1{"G1"};
|
||||
Account G2{"G2"};
|
||||
Account M1{"M1"};
|
||||
Account M2{"M2"};
|
||||
Account M3{"M3"};
|
||||
Account M4{"M4"};
|
||||
Account M5{"M5"};
|
||||
Account M6{"M6"};
|
||||
Account MM{"MM"};
|
||||
|
||||
env.fund(XRP(1000), A1, A2, G1, G2, M1, M2, M3, M4, M5, M6, MM);
|
||||
env.close();
|
||||
|
||||
env.trust(G1["HKD"](2000), A1);
|
||||
env.trust(G2["HKD"](2000), A2);
|
||||
|
||||
env.trust(G1["HKD"](100000), M1, M2, M3, M4, M5, M6, MM);
|
||||
env.trust(G2["HKD"](100000), M1, M2, M3, M4, M5, M6, MM);
|
||||
env.close();
|
||||
|
||||
env(pay(G1, A1, G1["HKD"](1000)));
|
||||
|
||||
env(pay(G1, M1, G1["HKD"](10)));
|
||||
env(pay(G1, M2, G1["HKD"](10)));
|
||||
env(pay(G1, M3, G1["HKD"](10)));
|
||||
env(pay(G1, M4, G1["HKD"](10)));
|
||||
env(pay(G1, M5, G1["HKD"](10)));
|
||||
env(pay(G1, M6, G1["HKD"](10)));
|
||||
env(pay(G1, MM, G1["HKD"](1000)));
|
||||
|
||||
env(pay(G2, M1, G2["HKD"](10)));
|
||||
env(pay(G2, M2, G2["HKD"](10)));
|
||||
env(pay(G2, M3, G2["HKD"](10)));
|
||||
env(pay(G2, M4, G2["HKD"](10)));
|
||||
env(pay(G2, M5, G2["HKD"](10)));
|
||||
env(pay(G2, M6, G2["HKD"](10)));
|
||||
env(pay(G2, MM, G2["HKD"](1000)));
|
||||
env.close();
|
||||
|
||||
env(offer(MM, G1["HKD"](1000), G2["HKD"](100)));
|
||||
env.close();
|
||||
|
||||
auto cache = std::make_shared<RippleLineCache>(
|
||||
env.current(), env.app().journal("RippleLineCache"));
|
||||
Pathfinder pf(
|
||||
cache,
|
||||
A1.id(),
|
||||
A2.id(),
|
||||
G1["HKD"].currency,
|
||||
std::nullopt,
|
||||
A2["HKD"](60),
|
||||
std::nullopt,
|
||||
env.app());
|
||||
|
||||
BEAST_EXPECT(pf.findPaths(7));
|
||||
pf.computePathRanks(5);
|
||||
|
||||
STPath fullLiquidityPath;
|
||||
auto bestPaths =
|
||||
pf.getBestPaths(5, fullLiquidityPath, STPathSet{}, A1.id());
|
||||
BEAST_EXPECT(bestPaths.size() == 5);
|
||||
BEAST_EXPECT(!fullLiquidityPath.empty());
|
||||
|
||||
if (!fullLiquidityPath.empty())
|
||||
bestPaths.push_back(fullLiquidityPath);
|
||||
BEAST_EXPECT(bestPaths.size() == 6);
|
||||
}
|
||||
|
||||
void
|
||||
issues_path_negative_issue()
|
||||
{
|
||||
@@ -1381,6 +1541,8 @@ public:
|
||||
path_find_04();
|
||||
path_find_05();
|
||||
path_find_06();
|
||||
pathfind_paths_computed_never_exceeds_six();
|
||||
pathfind_can_return_six_paths_with_append();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <test/jtx/tags.h>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
@@ -66,6 +67,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_), sig(acct_), nested(std::move(nested_))
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
isNested() const
|
||||
{
|
||||
return !nested.empty();
|
||||
}
|
||||
|
||||
AccountID
|
||||
id() const
|
||||
{
|
||||
return acct.id();
|
||||
}
|
||||
};
|
||||
|
||||
using SignerPtr = std::shared_ptr<Signer>;
|
||||
|
||||
// For backward compatibility
|
||||
struct Reg
|
||||
{
|
||||
Account acct;
|
||||
@@ -74,16 +117,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)
|
||||
{
|
||||
@@ -94,13 +134,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>
|
||||
requires std::convertible_to<AccountType, Reg>
|
||||
explicit msig(AccountType&& a0, Accounts&&... aN)
|
||||
@@ -114,6 +173,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. */
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <regex>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -137,7 +136,9 @@ class Invariants_test : public beast::unit_test::suite
|
||||
if (sink.messages().str().find(m) == std::string::npos)
|
||||
{
|
||||
// uncomment if you want to log the invariant failure
|
||||
// message log << " --> " << m << std::endl;
|
||||
// message
|
||||
// log << sink.messages().str() << std::endl;
|
||||
// log << " --> " << m << std::endl;
|
||||
fail();
|
||||
}
|
||||
}
|
||||
@@ -1233,6 +1234,42 @@ class Invariants_test : public beast::unit_test::suite
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
void
|
||||
testLockedBalance()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase << "ValidLockedBalance";
|
||||
|
||||
doInvariantCheck(
|
||||
{{"Invariant failed: IOU locked balance is greater than balance"}},
|
||||
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
IOU const USD{A2["USD"]};
|
||||
auto const sle =
|
||||
std::make_shared<SLE>(keylet::line(A1, A2, USD.currency));
|
||||
sle->setFieldAmount(sfHighLimit, A1["USD"](100));
|
||||
sle->setFieldAmount(sfLowLimit, A2["USD"](100));
|
||||
sle->setFieldAmount(sfBalance, USD(100));
|
||||
sle->setFieldAmount(sfLockedBalance, USD(101));
|
||||
ac.view().insert(sle);
|
||||
return true;
|
||||
});
|
||||
|
||||
doInvariantCheck(
|
||||
{{"Invariant failed: IOU locked balance is greater than balance"}},
|
||||
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
IOU const USD{A2["USD"]};
|
||||
auto const sle =
|
||||
std::make_shared<SLE>(keylet::line(A1, A2, USD.currency));
|
||||
sle->setFieldAmount(sfHighLimit, A1["USD"](100));
|
||||
sle->setFieldAmount(sfLowLimit, A2["USD"](100));
|
||||
sle->setFieldAmount(sfBalance, USD(-100));
|
||||
sle->setFieldAmount(sfLockedBalance, USD(-101));
|
||||
ac.view().insert(sle);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -1251,6 +1288,7 @@ public:
|
||||
testValidNewAccountRoot();
|
||||
testNFTokenPageInvariants();
|
||||
testPermissionedDomainInvariants();
|
||||
testLockedBalance();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ class Hooks_test : public beast::unit_test::suite
|
||||
sfHookAccount,
|
||||
sfEmittedTxn,
|
||||
sfHook,
|
||||
sfHookDefinition,
|
||||
sfHookParameter,
|
||||
sfHookGrant,
|
||||
sfEmitDetails,
|
||||
|
||||
@@ -1798,30 +1798,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.
|
||||
@@ -1831,12 +1833,21 @@ public:
|
||||
soTest1.setFieldVL(sfTxnSignature, saMultiSignature);
|
||||
testMalformedSigningAccount(soTest1, true);
|
||||
}
|
||||
|
||||
{
|
||||
// Test case 2. Omit sfSigningPubKey from SigningAccount.
|
||||
// Test case 2. Omit sfSigningPubKey from SigningAccount.
|
||||
// With nested multi-sign, sfSigningPubKey is optional in the
|
||||
// template, so serialization succeeds. The signer-shape helpers
|
||||
// still reject {Account + TxnSignature} as neither leaf nor nested.
|
||||
STObject soTest2(sfSigner);
|
||||
soTest2.setAccountID(sfAccount, id2);
|
||||
soTest2.setFieldVL(sfTxnSignature, saMultiSignature);
|
||||
testMalformedSigningAccount(soTest2, false);
|
||||
testMalformedSigningAccount(soTest2, true);
|
||||
|
||||
soTest2.applyTemplateFromSField(sfSigner);
|
||||
BEAST_EXPECT(!isLeafSigner(soTest2));
|
||||
BEAST_EXPECT(!isNestedSigner(soTest2));
|
||||
BEAST_EXPECT(!isValidSignerEntry(soTest2));
|
||||
}
|
||||
{
|
||||
// Test case 3. Extra sfAmount in SigningAccount.
|
||||
|
||||
@@ -332,6 +332,7 @@ multi_runner_child::run_multi(Pred pred)
|
||||
{
|
||||
if (!pred(*t))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
failed = run(*t) || failed;
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
#include <xrpld/core/TimeKeeper.h>
|
||||
#include <xrpld/net/RPCCall.h>
|
||||
#include <xrpld/rpc/RPCHandler.h>
|
||||
#include <xrpld/rpc/handlers/Handlers.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
@@ -388,8 +387,7 @@ run(int argc, char** argv)
|
||||
po::value<std::string>(),
|
||||
"Specify the range of present ledgers for testing purposes. Min and "
|
||||
"max values are comma separated.")(
|
||||
"version", "Display the build version.")(
|
||||
"definitions", "Output server definitions as JSON and exit.");
|
||||
"version", "Display the build version.");
|
||||
|
||||
po::options_description data("Ledger/Data Options");
|
||||
data.add_options()("import", importText.c_str())(
|
||||
@@ -531,13 +529,6 @@ run(int argc, char** argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (vm.count("definitions"))
|
||||
{
|
||||
auto defs = getStaticServerDefinitions();
|
||||
std::cout << Json::FastWriter().write(defs);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifndef ENABLE_TESTS
|
||||
if (vm.count("unittest") || vm.count("unittest-child"))
|
||||
{
|
||||
|
||||
@@ -170,7 +170,7 @@ private:
|
||||
std::chrono::steady_clock::time_point quick_reply_;
|
||||
std::chrono::steady_clock::time_point full_reply_;
|
||||
|
||||
static unsigned int const max_paths_ = 4;
|
||||
static unsigned int const max_paths_ = 5;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -1324,11 +1324,13 @@ Pathfinder::initPathTable()
|
||||
|
||||
fillPaths(
|
||||
pt_nonXRP_to_XRP,
|
||||
{{1, "sxd"}, // gateway buys XRP
|
||||
{2, "saxd"}, // source -> gateway -> book(XRP) -> dest
|
||||
{{1, "sxd"}, // gateway buys XRP
|
||||
{2, "saxd"}, // source -> gateway -> book(XRP) -> dest
|
||||
{5, "sabxd"}, // source -> gateway -> book -> book(XRP) -> dest
|
||||
{6, "saaxd"},
|
||||
{7, "sbxd"},
|
||||
{8, "sabxd"},
|
||||
{8,
|
||||
"sabbxd"}, // source -> gateway -> book -> book -> book(XRP) -> dest
|
||||
{9, "sabaxd"}});
|
||||
|
||||
// non-XRP to non-XRP (same currency)
|
||||
|
||||
@@ -1982,4 +1982,44 @@ ValidAMM::finalize(
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
ValidLockedBalance::visitEntry(
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after)
|
||||
{
|
||||
if (after && after->getType() == ltRIPPLE_STATE &&
|
||||
after->isFieldPresent(sfLockedBalance))
|
||||
{
|
||||
iouIOULockedBalanceAfter_ = (*after)[sfLockedBalance];
|
||||
iouIOUBalanceAfter_ = (*after)[sfBalance];
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ValidLockedBalance::finalize(
|
||||
STTx const& tx,
|
||||
TER const result,
|
||||
XRPAmount const,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
if (!view.rules().enabled(fixIOULockedBalanceInvariant))
|
||||
return true;
|
||||
|
||||
if (iouIOULockedBalanceAfter_)
|
||||
{
|
||||
if ((!iouIOULockedBalanceAfter_->negative() &&
|
||||
*iouIOULockedBalanceAfter_ > *iouIOUBalanceAfter_) ||
|
||||
(iouIOULockedBalanceAfter_->negative() &&
|
||||
*iouIOULockedBalanceAfter_ < *iouIOUBalanceAfter_))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: IOU locked balance is "
|
||||
"greater than balance";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace ripple
|
||||
|
||||
@@ -680,6 +680,30 @@ private:
|
||||
beast::Journal const&) const;
|
||||
};
|
||||
|
||||
class ValidLockedBalance
|
||||
{
|
||||
std::optional<STAmount> iouIOULockedBalanceAfter_;
|
||||
std::optional<STAmount> iouIOUBalanceAfter_;
|
||||
|
||||
public:
|
||||
ValidLockedBalance()
|
||||
{
|
||||
}
|
||||
void
|
||||
visitEntry(
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(
|
||||
STTx const&,
|
||||
TER const,
|
||||
XRPAmount const,
|
||||
ReadView const&,
|
||||
beast::Journal const&);
|
||||
};
|
||||
|
||||
// additional invariant checks can be declared above and then added to this
|
||||
// tuple
|
||||
using InvariantChecks = std::tuple<
|
||||
@@ -700,7 +724,8 @@ using InvariantChecks = std::tuple<
|
||||
ValidClawback,
|
||||
ValidMPTIssuance,
|
||||
ValidPermissionedDomain,
|
||||
ValidAMM>;
|
||||
ValidAMM,
|
||||
ValidLockedBalance>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
|
||||
@@ -425,7 +425,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
||||
JLOG(ctx.j.trace())
|
||||
<< "HookSet(" << hook::log::NAMESPACE_MISSING << ")["
|
||||
<< HS_ACC()
|
||||
<< "]: Malformed transaction: SetHook sfHookDefinition "
|
||||
<< "]: Malformed transaction: SetHook ltHookDefinition "
|
||||
"must contain sfHookNamespace.";
|
||||
return false;
|
||||
}
|
||||
@@ -2001,7 +2001,7 @@ SetHook::setHook()
|
||||
// sfHookHash, sfHookNamespace, sfHookOn, sfHookOnOutgoing,
|
||||
// sfHookOnIncoming, sfHookCanEmit sfHookApiVersion, sfFlags: free
|
||||
|
||||
// sfHookDefinition is not reserved because it is an unowned object,
|
||||
// ltHookDefinition is not reserved because it is an unowned object,
|
||||
// rather the uploader is billed via fee according to the following:
|
||||
// sfCreateCode: 5000 drops per byte
|
||||
// sfHookParameters: 5000 drops per byte
|
||||
|
||||
@@ -38,8 +38,12 @@
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
|
||||
namespace ripple {
|
||||
@@ -317,8 +321,36 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
|
||||
// Each signer adds one more baseFee to the minimum required fee
|
||||
// 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))
|
||||
{
|
||||
// Depth guard to prevent stack overflow from malicious deep nesting.
|
||||
int const maxDepth = view.rules().enabled(featureNestedMultiSign)
|
||||
? nestedMultiSignMaxDepth
|
||||
: legacyMultiSignMaxDepth;
|
||||
|
||||
std::function<std::size_t(STArray const&, int)> countSigners;
|
||||
|
||||
countSigners = [&](STArray const& signers, int depth) -> std::size_t {
|
||||
if (depth > maxDepth)
|
||||
return 0;
|
||||
|
||||
std::size_t count = 0;
|
||||
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
if (isNestedSigner(signer))
|
||||
count += countSigners(
|
||||
signer.getFieldArray(sfSigners), depth + 1);
|
||||
else if (isLeafSigner(signer))
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
signerCount = countSigners(tx.getFieldArray(sfSigners), 1);
|
||||
}
|
||||
|
||||
XRPAmount hookExecutionFee{0};
|
||||
uint64_t burden{1};
|
||||
@@ -972,167 +1004,273 @@ 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.
|
||||
XRPL_ASSERT(
|
||||
sleAccountSigners->isFieldPresent(sfSignerListID),
|
||||
"ripple::Transactor::checkMultiSign : has signer list ID");
|
||||
XRPL_ASSERT(
|
||||
sleAccountSigners->getFieldU32(sfSignerListID) == 0,
|
||||
"ripple::Transactor::checkMultiSign : signer list ID is 0");
|
||||
bool const allowNested = ctx.view.rules().enabled(featureNestedMultiSign);
|
||||
int const maxDepth =
|
||||
allowNested ? nestedMultiSignMaxDepth : legacyMultiSignMaxDepth;
|
||||
|
||||
auto accountSigners =
|
||||
SignerEntries::deserialize(*sleAccountSigners, ctx.j, "ledger");
|
||||
if (!accountSigners)
|
||||
return accountSigners.error();
|
||||
// Nested multi-sign is dynamic account delegation: a parent SignerList
|
||||
// authorizes signer accounts, not a frozen set of leaf keys. A nested
|
||||
// signer contributes when that signer account's current on-ledger
|
||||
// SignerList is satisfied by the transaction evidence.
|
||||
//
|
||||
// ancestors tracks the current proof path. An authorized signer entry that
|
||||
// points back to an ancestor is unavailable on that path and may cause the
|
||||
// local quorum to be cycle-adjusted below.
|
||||
std::function<NotTEC(
|
||||
AccountID const&, STArray const&, int, std::set<AccountID>)>
|
||||
validateSigners;
|
||||
|
||||
// Get the array of transaction signers.
|
||||
STArray const& txSigners(ctx.tx.getFieldArray(sfSigners));
|
||||
validateSigners = [&](AccountID const& acc,
|
||||
STArray const& signers,
|
||||
int depth,
|
||||
std::set<AccountID> ancestors) -> NotTEC {
|
||||
// Cycle detection is handled per authorized edge in the loop below,
|
||||
// rather than by failing the delegated account outright.
|
||||
|
||||
// Walk the accountSigners performing a variety of checks and see if
|
||||
// the quorum is met.
|
||||
|
||||
// Both the multiSigners and accountSigners are sorted by account. So
|
||||
// matching multi-signers to account signers should be a simple
|
||||
// linear walk. *All* signers must be valid or the transaction fails.
|
||||
std::uint32_t weightSum = 0;
|
||||
auto iter = accountSigners->begin();
|
||||
for (auto const& txSigner : txSigners)
|
||||
{
|
||||
AccountID const txSignerAcctID = txSigner.getAccountID(sfAccount);
|
||||
|
||||
// Attempt to match the SignerEntry with a Signer;
|
||||
while (iter->account < txSignerAcctID)
|
||||
if (depth > maxDepth)
|
||||
{
|
||||
if (++iter == accountSigners->end())
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Multi-signing depth limit exceeded at "
|
||||
<< depth << " (max=" << maxDepth << ")";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
ancestors.insert(acc);
|
||||
|
||||
// Get the SignerList for the account we're validating signers for.
|
||||
std::shared_ptr<STLedgerEntry const> sleAllowedSigners =
|
||||
ctx.view.read(keylet::signers(acc));
|
||||
|
||||
if (!sleAllowedSigners)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "checkMultiSign: Account " << acc
|
||||
<< " not set up for multi-signing.";
|
||||
return tefNOT_MULTI_SIGNING;
|
||||
}
|
||||
|
||||
// We have plans to support multiple SignerLists in the future. The
|
||||
// presence and defaulted value of the SignerListID field will enable
|
||||
// that.
|
||||
XRPL_ASSERT(
|
||||
sleAllowedSigners->isFieldPresent(sfSignerListID),
|
||||
"ripple::Transactor::checkMultiSign : has signer list ID");
|
||||
XRPL_ASSERT(
|
||||
sleAllowedSigners->getFieldU32(sfSignerListID) == 0,
|
||||
"ripple::Transactor::checkMultiSign : signer list ID is 0");
|
||||
|
||||
uint32_t const quorum = sleAllowedSigners->getFieldU32(sfSignerQuorum);
|
||||
uint32_t sum{0};
|
||||
|
||||
auto allowedSigners =
|
||||
SignerEntries::deserialize(*sleAllowedSigners, ctx.j, "ledger");
|
||||
if (!allowedSigners)
|
||||
return allowedSigners.error();
|
||||
|
||||
// Build lookup map for signer validation and weight retrieval.
|
||||
std::map<AccountID, uint16_t> signerWeights;
|
||||
uint32_t totalWeight{0}, cyclicWeight{0};
|
||||
for (auto const& entry : *allowedSigners)
|
||||
{
|
||||
signerWeights[entry.account] = entry.weight;
|
||||
totalWeight += entry.weight;
|
||||
if (ancestors.count(entry.account))
|
||||
cyclicWeight += entry.weight;
|
||||
}
|
||||
|
||||
// Walk the signers array, validating each signer. Signers must be in
|
||||
// strict ascending order for consensus.
|
||||
std::optional<AccountID> prevSigner;
|
||||
|
||||
for (auto const& signerEntry : signers)
|
||||
{
|
||||
AccountID const signer = signerEntry.getAccountID(sfAccount);
|
||||
|
||||
if (prevSigner && signer <= *prevSigner)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "applyTransaction: Invalid SigningAccount.Account.";
|
||||
<< "checkMultiSign: Signers not in strict ascending order: "
|
||||
<< signer << " <= " << *prevSigner;
|
||||
return temMALFORMED;
|
||||
}
|
||||
prevSigner = signer;
|
||||
|
||||
// Parent-edge authorization is checked before cycle skipping so an
|
||||
// unauthorized cyclic entry is rejected, not silently ignored.
|
||||
auto const weightIt = signerWeights.find(signer);
|
||||
if (weightIt == signerWeights.end())
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Invalid signer " << signer
|
||||
<< " not in signer list for " << acc;
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
}
|
||||
if (iter->account != txSignerAcctID)
|
||||
{
|
||||
// The SigningAccount is not in the SignerEntries.
|
||||
JLOG(ctx.j.trace())
|
||||
<< "applyTransaction: Invalid SigningAccount.Account.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
// We found the SigningAccount in the list of valid signers. Now we
|
||||
// need to compute the accountID that is associated with the signer's
|
||||
// public key.
|
||||
auto const spk = txSigner.getFieldVL(sfSigningPubKey);
|
||||
|
||||
if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk)))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: signing public key type is unknown";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
// This ternary is only needed to handle `simulate`
|
||||
XRPL_ASSERT(
|
||||
(ctx.flags & tapDRY_RUN) || !spk.empty(),
|
||||
"ripple::Transactor::checkMultiSign : non-empty signer or "
|
||||
"simulation");
|
||||
AccountID const signingAcctIDFromPubKey = spk.empty()
|
||||
? txSignerAcctID
|
||||
: calcAccountID(PublicKey(makeSlice(spk)));
|
||||
|
||||
// Verify that the signingAcctID and the signingAcctIDFromPubKey
|
||||
// belong together. Here are the rules:
|
||||
//
|
||||
// 1. "Phantom account": an account that is not in the ledger
|
||||
// A. If signingAcctID == signingAcctIDFromPubKey and the
|
||||
// signingAcctID is not in the ledger then we have a phantom
|
||||
// account.
|
||||
// B. Phantom accounts are always allowed as multi-signers.
|
||||
//
|
||||
// 2. "Master Key"
|
||||
// A. signingAcctID == signingAcctIDFromPubKey, and signingAcctID
|
||||
// is in the ledger.
|
||||
// B. If the signingAcctID in the ledger does not have the
|
||||
// asfDisableMaster flag set, then the signature is allowed.
|
||||
//
|
||||
// 3. "Regular Key"
|
||||
// A. signingAcctID != signingAcctIDFromPubKey, and signingAcctID
|
||||
// is in the ledger.
|
||||
// B. If signingAcctIDFromPubKey == signingAcctID.RegularKey (from
|
||||
// ledger) then the signature is allowed.
|
||||
//
|
||||
// No other signatures are allowed. (January 2015)
|
||||
|
||||
// In any of these cases we need to know whether the account is in
|
||||
// the ledger. Determine that now.
|
||||
auto sleTxSignerRoot = ctx.view.read(keylet::account(txSignerAcctID));
|
||||
|
||||
if (signingAcctIDFromPubKey == txSignerAcctID)
|
||||
{
|
||||
// Either Phantom or Master. Phantoms automatically pass.
|
||||
if (sleTxSignerRoot)
|
||||
// The signer is authorized by acc, but is already in the current
|
||||
// proof path. Treat that cyclic ancestor edge as unavailable for
|
||||
// this path; do not recurse into it and do not count its weight.
|
||||
if (ancestors.count(signer))
|
||||
{
|
||||
// Master Key. Account may not have asfDisableMaster set.
|
||||
std::uint32_t const signerAccountFlags =
|
||||
sleTxSignerRoot->getFieldU32(sfFlags);
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Skipping cyclic signer: " << signer;
|
||||
continue;
|
||||
}
|
||||
uint16_t const weight = weightIt->second;
|
||||
|
||||
if (signerAccountFlags & lsfDisableMaster)
|
||||
if (isNestedSigner(signerEntry))
|
||||
{
|
||||
STArray const& nestedSigners =
|
||||
signerEntry.getFieldArray(sfSigners);
|
||||
NotTEC result = validateSigners(
|
||||
signer, nestedSigners, depth + 1, ancestors);
|
||||
if (!isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
sum += weight;
|
||||
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Nested signer " << signer
|
||||
<< " validated, weight=" << weight << ", depth=" << depth
|
||||
<< ", sum=" << sum << "/" << quorum;
|
||||
}
|
||||
else if (isLeafSigner(signerEntry))
|
||||
{
|
||||
auto const spk = signerEntry.getFieldVL(sfSigningPubKey);
|
||||
|
||||
// spk being non-empty in non-simulate is checked in
|
||||
// STTx::checkMultiSign.
|
||||
if (!spk.empty() && !publicKeyType(makeSlice(spk)))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "applyTransaction: Signer:Account lsfDisableMaster.";
|
||||
return tefMASTER_DISABLED;
|
||||
<< "checkMultiSign: Unknown public key type for signer "
|
||||
<< signer;
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
(ctx.flags & tapDRY_RUN) || !spk.empty(),
|
||||
"ripple::Transactor::checkMultiSign : non-empty signer or "
|
||||
"simulation");
|
||||
AccountID const signingAcctIDFromPubKey = spk.empty()
|
||||
? signer
|
||||
: calcAccountID(PublicKey(makeSlice(spk)));
|
||||
|
||||
auto sleTxSignerRoot = ctx.view.read(keylet::account(signer));
|
||||
|
||||
if (signingAcctIDFromPubKey == signer)
|
||||
{
|
||||
// Either Phantom or Master. Phantoms automatically pass.
|
||||
if (sleTxSignerRoot)
|
||||
{
|
||||
// Master Key. Account may not have asfDisableMaster
|
||||
// set.
|
||||
std::uint32_t const signerAccountFlags =
|
||||
sleTxSignerRoot->getFieldU32(sfFlags);
|
||||
|
||||
if (signerAccountFlags & lsfDisableMaster)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Signer " << signer
|
||||
<< " has lsfDisableMaster set.";
|
||||
return tefMASTER_DISABLED;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// May be a Regular Key. Let's find out.
|
||||
if (!sleTxSignerRoot)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Non-phantom signer " << signer
|
||||
<< " lacks account root.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
|
||||
if (!sleTxSignerRoot->isFieldPresent(sfRegularKey))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "checkMultiSign: Signer "
|
||||
<< signer << " lacks RegularKey.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
if (signingAcctIDFromPubKey !=
|
||||
sleTxSignerRoot->getAccountID(sfRegularKey))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Signer " << signer
|
||||
<< " pubkey doesn't match RegularKey.";
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
}
|
||||
|
||||
sum += weight;
|
||||
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Leaf signer " << signer
|
||||
<< " validated, weight=" << weight << ", depth=" << depth
|
||||
<< ", sum=" << sum << "/" << quorum;
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkMultiSign: Malformed signer entry for " << signer;
|
||||
return tefBAD_SIGNATURE;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (cyclicWeight > totalWeight)
|
||||
{
|
||||
// May be a Regular Key. Let's find out.
|
||||
// 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))
|
||||
// Dynamic delegation still requires the delegated account's own policy
|
||||
// to be satisfied. The only adjustment is for authorized ancestor edges
|
||||
// that cannot be used without circular proof. If those cyclic edges
|
||||
// make the configured quorum unreachable, require all remaining
|
||||
// non-cyclic weight.
|
||||
uint32_t cycleAdjustedQuorum = quorum;
|
||||
uint32_t const maxAchievable = totalWeight - cyclicWeight;
|
||||
|
||||
if (cyclicWeight > 0 && maxAchievable < quorum)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "checkMultiSign: Cycle-adjusted quorum for "
|
||||
<< acc << ": " << quorum << " -> "
|
||||
<< maxAchievable << " (total=" << totalWeight
|
||||
<< ", cyclic=" << cyclicWeight << ")";
|
||||
cycleAdjustedQuorum = maxAchievable;
|
||||
}
|
||||
|
||||
if (cycleAdjustedQuorum == 0)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "checkMultiSign: All signers for " << acc
|
||||
<< " are cyclic - no valid signing path exists.";
|
||||
return tefBAD_QUORUM;
|
||||
}
|
||||
|
||||
if (sum < cycleAdjustedQuorum)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "checkMultiSign: Quorum not met for " << acc
|
||||
<< " at depth " << depth << " (sum=" << sum
|
||||
<< ", required=" << cycleAdjustedQuorum << ")";
|
||||
return tefBAD_QUORUM;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
};
|
||||
|
||||
STArray const& entries(ctx.tx.getFieldArray(sfSigners));
|
||||
|
||||
NotTEC result = validateSigners(id, entries, 1, {});
|
||||
if (!isTesSuccess(result))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "applyTransaction: Signers failed to meet quorum.";
|
||||
return tefBAD_QUORUM;
|
||||
<< "checkMultiSign: Validation failed with " << transToken(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Met the quorum. Continue.
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,11 @@
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
|
||||
namespace ripple {
|
||||
@@ -1318,19 +1320,31 @@ transactionSubmitMultiSigned(
|
||||
if (signers.empty())
|
||||
return RPC::make_param_error("tx_json.Signers array may not be empty.");
|
||||
|
||||
// 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);
|
||||
}) != signers.end())
|
||||
// Recursively validate signer entry structure (including nested ones).
|
||||
// Feature enablement is enforced later in preflight; RPC only validates
|
||||
// shape here.
|
||||
std::function<bool(STArray const&, int)> validateSignersRecursive;
|
||||
validateSignersRecursive = [&](STArray const& arr, int depth) -> bool {
|
||||
if (depth > nestedMultiSignMaxDepth)
|
||||
return false;
|
||||
|
||||
for (auto const& signer : arr)
|
||||
{
|
||||
if (!isValidSignerEntry(signer))
|
||||
return false;
|
||||
|
||||
if (isNestedSigner(signer) &&
|
||||
!validateSignersRecursive(
|
||||
signer.getFieldArray(sfSigners), depth + 1))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!validateSignersRecursive(signers, 1))
|
||||
{
|
||||
return RPC::make_param_error(
|
||||
"Signers array may only contain Signer entries.");
|
||||
"Signers array may only contain valid Signer entries.");
|
||||
}
|
||||
|
||||
// The array must be sorted and validated.
|
||||
|
||||
@@ -129,8 +129,6 @@ doRipplePathFind(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doServerDefinitions(RPC::JsonContext&);
|
||||
Json::Value
|
||||
getStaticServerDefinitions();
|
||||
Json::Value
|
||||
doServerInfo(RPC::JsonContext&); // for humans
|
||||
Json::Value
|
||||
doServerState(RPC::JsonContext&); // for machines
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <magic/magic_enum.h>
|
||||
#include <magic_enum.hpp>
|
||||
#include <sstream>
|
||||
|
||||
#define MAGIC_ENUM(x, _min, _max) \
|
||||
@@ -523,15 +523,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
Json::Value
|
||||
getStaticServerDefinitions()
|
||||
{
|
||||
static const Definitions defs{};
|
||||
Json::Value ret = defs();
|
||||
ret[jss::hash] = to_string(defs.getHash());
|
||||
return ret;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
doServerDefinitions(RPC::JsonContext& context)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user