mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-28 06:37:07 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6238cb7109 | ||
|
|
0cd4f6aae4 | ||
|
|
a4b0a90998 | ||
|
|
6191c1e09d | ||
|
|
6981b5b06b | ||
|
|
f1a3a775a1 | ||
|
|
d4a681430a | ||
|
|
3760e1fb41 | ||
|
|
c1dabe5576 | ||
|
|
6442e0990b | ||
|
|
1e6cda4d64 | ||
|
|
c146f15247 | ||
|
|
fb5081d1f4 | ||
|
|
d4bec012a2 | ||
|
|
f7187ba94f |
No files matched your search
@@ -205,6 +205,7 @@ xrpld.rpc > xrpld.core
|
||||
xrpld.rpc > xrpld.ledger
|
||||
xrpld.rpc > xrpld.nodestore
|
||||
xrpld.rpc > xrpld.shamap
|
||||
xrpld.rpc > xrpl.hook
|
||||
xrpld.rpc > xrpl.json
|
||||
xrpld.rpc > xrpl.protocol
|
||||
xrpld.rpc > xrpl.resource
|
||||
|
||||
@@ -494,7 +494,7 @@
|
||||
#
|
||||
# Configure the maximum number of transactions to have in the job queue
|
||||
#
|
||||
# Must be a number between 100 and 1000, defaults to 250
|
||||
# Must be a number between 100 and 1000, defaults to 1000
|
||||
#
|
||||
#
|
||||
# [overlay]
|
||||
@@ -593,7 +593,7 @@
|
||||
# reaches or exceeds this number. After that the limit may still
|
||||
# change, but will stay above the target. If consensus is not
|
||||
# healthy, the limit will be clamped to this value or lower.
|
||||
# Default: 50.
|
||||
# Default: 1000.
|
||||
#
|
||||
# maximum_txn_in_ledger = <number>
|
||||
#
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
#define sfEscrowID ((5U << 16U) + 35U)
|
||||
#define sfURITokenID ((5U << 16U) + 36U)
|
||||
#define sfDomainID ((5U << 16U) + 37U)
|
||||
#define sfManifestID ((5U << 16U) + 91U)
|
||||
#define sfHookOnOutgoing ((5U << 16U) + 93U)
|
||||
#define sfHookOnIncoming ((5U << 16U) + 94U)
|
||||
#define sfCron ((5U << 16U) + 95U)
|
||||
@@ -288,6 +289,7 @@
|
||||
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
|
||||
#define sfPriceData ((14U << 16U) + 32U)
|
||||
#define sfCredential ((14U << 16U) + 33U)
|
||||
#define sfManifest ((14U << 16U) + 90U)
|
||||
#define sfAmountEntry ((14U << 16U) + 91U)
|
||||
#define sfMintURIToken ((14U << 16U) + 92U)
|
||||
#define sfHookEmission ((14U << 16U) + 93U)
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
#define ttNFTOKEN_MODIFY 70
|
||||
#define ttPERMISSIONED_DOMAIN_SET 71
|
||||
#define ttPERMISSIONED_DOMAIN_DELETE 72
|
||||
#define ttMANIFEST_SET 91
|
||||
#define ttCRON 92
|
||||
#define ttCRON_SET 93
|
||||
#define ttREMARKS_SET 94
|
||||
|
||||
@@ -382,7 +382,7 @@ enum class hook_return_code : int64_t {
|
||||
INVALID_KEY = -41, // user supplied key was not valid
|
||||
NOT_A_STRING = -42, // nul terminator missing from a string argument
|
||||
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
|
||||
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 5000 modified state
|
||||
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 256 modified state
|
||||
// entires in the combined hook chains
|
||||
TOO_MANY_NAMESPACES = -45
|
||||
};
|
||||
|
||||
@@ -372,6 +372,10 @@ permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
|
||||
|
||||
Keylet
|
||||
permissionedDomain(uint256 const& domainID) noexcept;
|
||||
|
||||
Keylet
|
||||
manifest(PublicKey const& pk) noexcept;
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
|
||||
@@ -194,6 +194,8 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefNONDIR_EMIT,
|
||||
tefIMPORT_BLACKHOLED,
|
||||
tefINVALID_LEDGER_FIX_TYPE,
|
||||
tefPAST_MANIFEST_SEQ,
|
||||
tefREVOKED_MANIFEST,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -262,6 +262,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
|
||||
{sfHookStateScale, soeOPTIONAL},
|
||||
{sfCron, soeOPTIONAL},
|
||||
{sfAMMID, soeOPTIONAL},
|
||||
{sfManifestID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A ledger object which contains a list of object identifiers.
|
||||
@@ -592,6 +593,26 @@ LEDGER_ENTRY(ltDID, 0x008D, DID, did, ({
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
|
||||
{sfAccount, soeREQUIRED},
|
||||
{sfPublicKey, soeREQUIRED},
|
||||
{sfSigningPubKey, soeOPTIONAL}, /* may be absent if the master is revoked */
|
||||
{sfSequence, soeREQUIRED},
|
||||
/* sfVersion defaults to 0 and is omitted from the signed payload when
|
||||
absent. Storing it unconditionally would change the bytes and break
|
||||
signature verification when the manifest is reconstructed. */
|
||||
{sfVersion, soeOPTIONAL},
|
||||
{sfDomain, soeOPTIONAL},
|
||||
/* The signatures are mirrored so the object is a lossless copy of the
|
||||
manifest and can be independently verified, and re-served to peers,
|
||||
by any node reading it (see ManifestCache::applyLedger). */
|
||||
{sfMasterSignature, soeREQUIRED},
|
||||
{sfSignature, soeOPTIONAL}, /* absent if the master is revoked */
|
||||
{sfManifestID, soeOPTIONAL}, /* pointer to the identical other copy on the other key */
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
#undef EXPAND
|
||||
#undef LEDGER_ENTRY_DUPLICATE
|
||||
|
||||
@@ -210,6 +210,7 @@ TYPED_SFIELD(sfOfferID, UINT256, 34)
|
||||
TYPED_SFIELD(sfEscrowID, UINT256, 35)
|
||||
TYPED_SFIELD(sfURITokenID, UINT256, 36)
|
||||
TYPED_SFIELD(sfDomainID, UINT256, 37)
|
||||
TYPED_SFIELD(sfManifestID, UINT256, 91)
|
||||
TYPED_SFIELD(sfHookOnOutgoing, UINT256, 93)
|
||||
TYPED_SFIELD(sfHookOnIncoming, UINT256, 94)
|
||||
TYPED_SFIELD(sfCron, UINT256, 95)
|
||||
@@ -382,6 +383,7 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
|
||||
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
|
||||
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
|
||||
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
|
||||
UNTYPED_SFIELD(sfManifest, OBJECT, 90)
|
||||
UNTYPED_SFIELD(sfAmountEntry, OBJECT, 91)
|
||||
UNTYPED_SFIELD(sfMintURIToken, OBJECT, 92)
|
||||
UNTYPED_SFIELD(sfHookEmission, OBJECT, 93)
|
||||
|
||||
@@ -500,6 +500,12 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
|
||||
{sfDomainID, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/* sfAccount is supplied by TxFormats::commonFields; listing it here as well
|
||||
makes the SOTemplate reject the format at construction. */
|
||||
TRANSACTION(ttMANIFEST_SET, 91, SetManifest, ({
|
||||
{sfManifest, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
|
||||
TRANSACTION(ttCRON, 92, Cron, ({
|
||||
{sfOwner, soeREQUIRED},
|
||||
|
||||
@@ -350,6 +350,7 @@ JSS(hash_mismatches); // out: catalogue
|
||||
JSS(have_header); // out: InboundLedger
|
||||
JSS(have_state); // out: InboundLedger
|
||||
JSS(have_transactions); // out: InboundLedger
|
||||
JSS(hooks); // in/out: AccountInfo
|
||||
JSS(high); // out: BookChanges
|
||||
JSS(highest_sequence); // out: AccountInfo
|
||||
JSS(highest_ticket); // out: AccountInfo
|
||||
|
||||
@@ -89,6 +89,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
ORACLE = LEDGER_NAMESPACE2(0x01, 'R'),
|
||||
MPTOKEN_ISSUANCE = '~',
|
||||
MPTOKEN = 't',
|
||||
MANIFEST = 'M',
|
||||
CREDENTIAL = LEDGER_NAMESPACE2(0x01, 'D'),
|
||||
PERMISSIONED_DOMAIN = 'm',
|
||||
|
||||
@@ -670,6 +671,12 @@ permissionedDomain(uint256 const& domainID) noexcept
|
||||
return {ltPERMISSIONED_DOMAIN, domainID};
|
||||
}
|
||||
|
||||
Keylet
|
||||
manifest(PublicKey const& pk) noexcept
|
||||
{
|
||||
return {ltMANIFEST, indexHash(LedgerNameSpace::MANIFEST, pk.slice())};
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
@@ -151,6 +151,8 @@ transResults()
|
||||
MAKE_ERROR(tefNONDIR_EMIT, "An emitted txn was injected into the ledger without a corresponding directory entry."),
|
||||
MAKE_ERROR(tefIMPORT_BLACKHOLED, "Cannot import keying because target account is blackholed."),
|
||||
MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."),
|
||||
MAKE_ERROR(tefPAST_MANIFEST_SEQ, "The submitted manifest's sequence is not newer than the current."),
|
||||
MAKE_ERROR(tefREVOKED_MANIFEST, "The submitted manifest is for a revoked master key."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
|
||||
@@ -30,7 +30,10 @@
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/hook/Enum.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/PayChan.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <vector>
|
||||
|
||||
@@ -8445,6 +8448,150 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Builds a manifest signed by `master`, nominating `ephemeral` as the
|
||||
// signing key. A sequence of UINT32_MAX makes it a revocation, which by
|
||||
// definition carries no signing key.
|
||||
static std::string
|
||||
makeManifestString(
|
||||
jtx::Account const& master,
|
||||
jtx::Account const& ephemeral,
|
||||
std::uint32_t seq)
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = seq;
|
||||
st[sfPublicKey] = master.pk();
|
||||
|
||||
if (seq != std::numeric_limits<std::uint32_t>::max())
|
||||
{
|
||||
st[sfSigningPubKey] = ephemeral.pk();
|
||||
sign(
|
||||
st,
|
||||
HashPrefix::manifest,
|
||||
*publicKeyType(ephemeral.pk()),
|
||||
ephemeral.sk());
|
||||
}
|
||||
|
||||
sign(
|
||||
st,
|
||||
HashPrefix::manifest,
|
||||
*publicKeyType(master.pk()),
|
||||
master.sk(),
|
||||
sfMasterSignature);
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
return std::string(static_cast<char const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
// A manifest transaction carries no account signature, so it cannot be
|
||||
// submitted through env() the way a signed transaction can. Returns the
|
||||
// resulting transaction id so the caller can inspect its metadata.
|
||||
uint256
|
||||
submitManifest(jtx::Env& env, std::string const& manifest)
|
||||
{
|
||||
Json::Value params;
|
||||
params[jss::manifest] = strHex(manifest);
|
||||
auto const jrr = env.rpc("json", "submit", to_string(params));
|
||||
|
||||
auto const& result = jrr[jss::result];
|
||||
|
||||
if (!BEAST_EXPECT(
|
||||
result[jss::engine_result].asString() == "tesSUCCESS"))
|
||||
{
|
||||
log << "submitManifest: " << to_string(jrr) << std::endl;
|
||||
return beast::zero;
|
||||
}
|
||||
|
||||
// An error response carries no tx_json, and strUnHex("") yields an
|
||||
// engaged but empty Blob, so fromVoid() would memcpy from nullptr.
|
||||
auto const blob = strUnHex(result[jss::tx_json][jss::hash].asString());
|
||||
auto const hash =
|
||||
blob ? uint256::fromVoidChecked(*blob) : std::optional<uint256>{};
|
||||
|
||||
if (!BEAST_EXPECT(hash.has_value()))
|
||||
{
|
||||
log << "submitManifest: " << to_string(jrr) << std::endl;
|
||||
return beast::zero;
|
||||
}
|
||||
|
||||
return *hash;
|
||||
}
|
||||
|
||||
// SetManifest
|
||||
// | otxn | tsh | manifest |
|
||||
// | M | M | N/A |
|
||||
// | M | E | W | ephemeral key's logical account
|
||||
void
|
||||
testSetManifestTSH(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
testcase("set manifest TSH");
|
||||
|
||||
if (!features[featureOnChainManifests])
|
||||
return;
|
||||
|
||||
// otxn: master
|
||||
// tsh: ephemeral
|
||||
// w/s: weak
|
||||
//
|
||||
// The ephemeral account is only named by the manifest, so it may
|
||||
// observe the transaction but not rollback it. It therefore fires only
|
||||
// when it has asked to collect.
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master, ephemeral);
|
||||
env.close();
|
||||
|
||||
if (!testStrong)
|
||||
addWeakTSH(env, ephemeral);
|
||||
|
||||
setTSHHook(env, ephemeral, testStrong);
|
||||
|
||||
auto const txHash =
|
||||
submitManifest(env, makeManifestString(master, ephemeral, 1));
|
||||
env.close();
|
||||
|
||||
// A strong hook on a weak stake holder is never reached.
|
||||
auto const expected = testStrong ? tshNONE : tshWEAK;
|
||||
testTSHStrongWeak(env, txHash, expected, __LINE__);
|
||||
}
|
||||
|
||||
// A revocation names no signing key, so there is no ephemeral stake
|
||||
// holder to notify at all.
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master, ephemeral);
|
||||
env.close();
|
||||
|
||||
addWeakTSH(env, ephemeral);
|
||||
setTSHHook(env, ephemeral, false);
|
||||
|
||||
auto const txHash = submitManifest(
|
||||
env,
|
||||
makeManifestString(
|
||||
master,
|
||||
ephemeral,
|
||||
std::numeric_limits<std::uint32_t>::max()));
|
||||
env.close();
|
||||
|
||||
testTSHStrongWeak(env, txHash, tshNONE, __LINE__);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testTSH(FeatureBitset features)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,819 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/network.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/detail/SetManifest.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/ledger/OpenView.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol/st.h>
|
||||
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
/** Tests the OnChainManifests amendment: the SetManifest transactor, the
|
||||
submit RPC path that builds its transaction, and reading manifests back out
|
||||
of the ledger into the manifest cache.
|
||||
*/
|
||||
struct SetManifest_test : public beast::unit_test::suite
|
||||
{
|
||||
// A manifest transaction is unsigned, so the network id is mandatory and
|
||||
// the network must be one that requires it (id > 1024).
|
||||
static std::unique_ptr<Config>
|
||||
makeConfig()
|
||||
{
|
||||
return jtx::network::makeNetworkConfig(
|
||||
21337, "10", "1000000", "200000");
|
||||
}
|
||||
|
||||
/** Builds a manifest signed by `master` nominating `ephemeral`.
|
||||
|
||||
A sequence of UINT32_MAX makes it a revocation, which by definition
|
||||
names no signing key and carries no ephemeral signature.
|
||||
*/
|
||||
static std::string
|
||||
makeManifest(
|
||||
jtx::Account const& master,
|
||||
jtx::Account const& ephemeral,
|
||||
std::uint32_t seq)
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = seq;
|
||||
st[sfPublicKey] = master.pk();
|
||||
|
||||
if (seq != std::numeric_limits<std::uint32_t>::max())
|
||||
{
|
||||
st[sfSigningPubKey] = ephemeral.pk();
|
||||
sign(
|
||||
st,
|
||||
HashPrefix::manifest,
|
||||
*publicKeyType(ephemeral.pk()),
|
||||
ephemeral.sk());
|
||||
}
|
||||
|
||||
sign(
|
||||
st,
|
||||
HashPrefix::manifest,
|
||||
*publicKeyType(master.pk()),
|
||||
master.sk(),
|
||||
sfMasterSignature);
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
return std::string(static_cast<char const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
/** Submits a manifest through the submit RPC.
|
||||
|
||||
The transaction is unsigned -- authority comes from the manifest's own
|
||||
master and ephemeral signatures -- so it cannot be submitted through
|
||||
env() the way a signed transaction can.
|
||||
*/
|
||||
static Json::Value
|
||||
submit(jtx::Env& env, std::string const& manifest)
|
||||
{
|
||||
Json::Value params;
|
||||
params[jss::manifest] = strHex(manifest);
|
||||
return env.rpc("json", "submit", to_string(params))[jss::result];
|
||||
}
|
||||
|
||||
static std::string
|
||||
engineResult(Json::Value const& result)
|
||||
{
|
||||
return result[jss::engine_result].asString();
|
||||
}
|
||||
|
||||
/** A well formed STObject that is not a well formed manifest.
|
||||
|
||||
The manifest format requires sfPublicKey and sfMasterSignature, so this
|
||||
parses as an object and then fails applyTemplate.
|
||||
*/
|
||||
static std::string
|
||||
makeUnparseableManifest()
|
||||
{
|
||||
STObject st(sfGeneric);
|
||||
st[sfSequence] = 1;
|
||||
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
return std::string(static_cast<char const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
/** The envelope Submit would build for `manifest`, optionally tweaked.
|
||||
|
||||
makeSetManifestTx() rejects any manifest the transactor would reject,
|
||||
and checkValidity() rejects any envelope whose signatures do not check
|
||||
out, so several of the transactor's own rejections cannot be provoked
|
||||
through the submit RPC at all. Building the envelope here and applying
|
||||
it straight to the open view reaches them.
|
||||
*/
|
||||
static std::shared_ptr<STTx const>
|
||||
envelope(
|
||||
jtx::Env& env,
|
||||
std::string const& manifest,
|
||||
AccountID const& account,
|
||||
std::function<void(STObject&)> const& tweak = {})
|
||||
{
|
||||
auto const build = [&](XRPAmount fee, bool tweaked) {
|
||||
return std::make_shared<STTx const>(
|
||||
ttMANIFEST_SET, [&](STObject& obj) {
|
||||
obj.setAccountID(sfAccount, account);
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
obj.setFieldU32(sfNetworkID, env.app().config().NETWORK_ID);
|
||||
obj.setFieldAmount(sfFee, fee);
|
||||
obj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
obj.setFieldVL(sfTxnSignature, Blob{});
|
||||
|
||||
SerialIter mit{makeSlice(manifest)};
|
||||
obj.peekFieldObject(sfManifest).set(mit);
|
||||
|
||||
if (tweaked && tweak)
|
||||
tweak(obj);
|
||||
});
|
||||
};
|
||||
|
||||
// The same two-pass pricing makeSetManifestTx() does: encode once with
|
||||
// a placeholder purely to have something to measure, then encode at the
|
||||
// ceiling checkFee() will accept.
|
||||
auto const base = SetManifest::calculateBaseFee(
|
||||
*env.current(), *build(XRPAmount{0}, false));
|
||||
|
||||
return build(mulRatio(base, 12, 10, /*roundUp*/ true), true);
|
||||
}
|
||||
|
||||
/** A mutable copy of a ledger object, suitable for the RawView interface.
|
||||
|
||||
Round-tripped through the wire format rather than copy constructed.
|
||||
applyTemplate() rejects an object carrying a materialised soeDEFAULT
|
||||
field that holds its default value, and that is exactly what reading one
|
||||
out of a view hands back: an AccountRoot arrives with sfMintedNFTokens
|
||||
present and zero. Serializing omits those fields and deserializing
|
||||
re-materialises them.
|
||||
*/
|
||||
static std::shared_ptr<SLE>
|
||||
rawCopy(std::shared_ptr<SLE const> const& sle)
|
||||
{
|
||||
Serializer s;
|
||||
sle->add(s);
|
||||
SerialIter sit{s.slice()};
|
||||
return std::make_shared<SLE>(sit, sle->key());
|
||||
}
|
||||
|
||||
/** Removes a ledger object from a view, if present.
|
||||
|
||||
OpenView exposes only the RawView interface, so the entry has to be
|
||||
copied off the read side before it can be handed back to rawErase.
|
||||
*/
|
||||
static void
|
||||
rawErase(OpenView& view, Keylet const& kl)
|
||||
{
|
||||
if (auto const sle = view.read(kl))
|
||||
view.rawErase(rawCopy(sle));
|
||||
}
|
||||
|
||||
/** Applies `tx` to a throwaway copy of the open ledger and reports the TER.
|
||||
|
||||
`prepare` runs against the same view first, so a test can corrupt state
|
||||
that no ordinary transaction could produce. Nothing is retained.
|
||||
*/
|
||||
TER
|
||||
applyDirect(
|
||||
jtx::Env& env,
|
||||
std::shared_ptr<STTx const> const& tx,
|
||||
std::function<void(OpenView&)> const& prepare = {})
|
||||
{
|
||||
TER ret = tesSUCCESS;
|
||||
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
|
||||
if (prepare)
|
||||
prepare(view);
|
||||
ret = ripple::apply(env.app(), view, *tx, tapNONE, j).ter;
|
||||
return false; // discard, corrupt or otherwise
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
void
|
||||
testSubmission(FeatureBitset features)
|
||||
{
|
||||
testcase("submission");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, ephemeral, 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
// A manifest is written twice so it can be found from either key, and
|
||||
// each copy points at the other.
|
||||
auto const byMaster = env.le(keylet::manifest(master.pk()));
|
||||
auto const byEphemeral = env.le(keylet::manifest(ephemeral.pk()));
|
||||
if (!BEAST_EXPECT(byMaster))
|
||||
return;
|
||||
if (!BEAST_EXPECT(byEphemeral))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(byMaster->getAccountID(sfAccount) == master.id());
|
||||
BEAST_EXPECT(byEphemeral->getAccountID(sfAccount) == master.id());
|
||||
BEAST_EXPECT(byMaster->getFieldU32(sfSequence) == 1);
|
||||
BEAST_EXPECT(
|
||||
byMaster->getFieldH256(sfManifestID) ==
|
||||
keylet::manifest(ephemeral.pk()).key);
|
||||
BEAST_EXPECT(
|
||||
byEphemeral->getFieldH256(sfManifestID) ==
|
||||
keylet::manifest(master.pk()).key);
|
||||
|
||||
// Both signatures are mirrored so the object can be verified, and
|
||||
// re-served, by any node that reads it.
|
||||
BEAST_EXPECT(byMaster->isFieldPresent(sfMasterSignature));
|
||||
BEAST_EXPECT(byMaster->isFieldPresent(sfSignature));
|
||||
|
||||
// The account root points at the master key's copy.
|
||||
auto const sleAcct = env.le(master);
|
||||
if (!BEAST_EXPECT(sleAcct))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
sleAcct->getFieldH256(sfManifestID) ==
|
||||
keylet::manifest(master.pk()).key);
|
||||
|
||||
// The account sequence must be untouched: the transaction is unsigned
|
||||
// and pinned to sequence 0, so consuming a sequence would let a third
|
||||
// party burn the validator's sequence numbers -- and writing seq + 1
|
||||
// would reset the account to 1.
|
||||
BEAST_EXPECT(sleAcct->getFieldU32(sfSequence) == env.seq(master));
|
||||
}
|
||||
|
||||
void
|
||||
testUpdate(FeatureBitset features)
|
||||
{
|
||||
testcase("update and stale rejection");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const eph1 = Account("eph1", KeyType::ed25519);
|
||||
auto const eph2 = Account("eph2", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master);
|
||||
env.close();
|
||||
|
||||
submit(env, makeManifest(master, eph1, 1));
|
||||
env.close();
|
||||
|
||||
// Rotating the ephemeral key erases both old copies and writes two
|
||||
// new ones, so the two can never drift apart.
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, eph2, 2))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(eph1.pk())));
|
||||
if (!BEAST_EXPECT(env.le(keylet::manifest(eph2.pk()))))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::manifest(master.pk()))->getFieldU32(sfSequence) ==
|
||||
2);
|
||||
|
||||
// Replaying the manifest we just applied, and anything older, is
|
||||
// rejected on sequence. This is what prevents replay: the transaction
|
||||
// is unsigned, so nothing else would.
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, eph2, 2))) ==
|
||||
"tefPAST_MANIFEST_SEQ");
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, eph1, 1))) ==
|
||||
"tefPAST_MANIFEST_SEQ");
|
||||
}
|
||||
|
||||
void
|
||||
testRevocation(FeatureBitset features)
|
||||
{
|
||||
testcase("revocation");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master);
|
||||
env.close();
|
||||
|
||||
submit(env, makeManifest(master, ephemeral, 1));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(
|
||||
env,
|
||||
makeManifest(
|
||||
master,
|
||||
ephemeral,
|
||||
std::numeric_limits<std::uint32_t>::max()))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
// A revocation has no signing key, so only the master key's copy
|
||||
// exists and it points at nothing.
|
||||
auto const byMaster = env.le(keylet::manifest(master.pk()));
|
||||
if (!BEAST_EXPECT(byMaster))
|
||||
return;
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(ephemeral.pk())));
|
||||
BEAST_EXPECT(!byMaster->isFieldPresent(sfManifestID));
|
||||
BEAST_EXPECT(!byMaster->isFieldPresent(sfSigningPubKey));
|
||||
BEAST_EXPECT(
|
||||
byMaster->getFieldU32(sfSequence) ==
|
||||
std::numeric_limits<std::uint32_t>::max());
|
||||
|
||||
// Nothing supersedes a revocation.
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, ephemeral, 2))) ==
|
||||
"tefREVOKED_MANIFEST");
|
||||
}
|
||||
|
||||
void
|
||||
testRetrieval(FeatureBitset features)
|
||||
{
|
||||
testcase("retrieval into the manifest cache");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const eph1 = Account("eph1", KeyType::ed25519);
|
||||
auto const eph2 = Account("eph2", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master);
|
||||
env.close();
|
||||
|
||||
submit(env, makeManifest(master, eph1, 1));
|
||||
env.close();
|
||||
|
||||
auto& cache = env.app().validatorManifests();
|
||||
|
||||
// Nothing has fed the cache yet, so an unknown key maps to itself.
|
||||
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
|
||||
|
||||
// Reading the ledger resolves both directions of the mapping. The
|
||||
// manifest is reconstructed from the ledger object and verified, so a
|
||||
// lossy round trip would fail here rather than be accepted.
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
|
||||
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == master.pk());
|
||||
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph1.pk());
|
||||
BEAST_EXPECT(cache.getSequence(master.pk()) == 1);
|
||||
|
||||
// Applying the same ledger again is a no-op: the cache is already at
|
||||
// that sequence.
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 0);
|
||||
|
||||
// A rotation on-ledger is picked up, and the superseded ephemeral key
|
||||
// stops resolving.
|
||||
submit(env, makeManifest(master, eph2, 2));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
|
||||
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph2.pk());
|
||||
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
|
||||
|
||||
// A key we never ask about is never read.
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed(), {eph2.pk()}) == 0);
|
||||
|
||||
// A revocation reaches the cache too, and is the one thing that must
|
||||
// never be lost: forgetting it would mean trusting the old key again.
|
||||
submit(
|
||||
env,
|
||||
makeManifest(
|
||||
master, eph2, std::numeric_limits<std::uint32_t>::max()));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
|
||||
BEAST_EXPECT(cache.revoked(master.pk()));
|
||||
}
|
||||
|
||||
void
|
||||
testSigningKeyRetrieval(FeatureBitset features)
|
||||
{
|
||||
testcase("retrieval by ephemeral key");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const eph1 = Account("eph1", KeyType::ed25519);
|
||||
auto const eph2 = Account("eph2", KeyType::ed25519);
|
||||
auto const stranger = Account("stranger", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master);
|
||||
env.close();
|
||||
|
||||
submit(env, makeManifest(master, eph1, 1));
|
||||
env.close();
|
||||
|
||||
auto& cache = env.app().validatorManifests();
|
||||
|
||||
// The situation applyLedger() cannot serve: a validation arrives
|
||||
// signed by eph1 and the node holds no manifest naming it, so the
|
||||
// master key to probe for is exactly what is missing.
|
||||
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
|
||||
|
||||
BEAST_EXPECT(
|
||||
cache.applyLedgerSigningKey(*env.closed(), eph1.pk()) ==
|
||||
master.pk());
|
||||
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == master.pk());
|
||||
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph1.pk());
|
||||
BEAST_EXPECT(cache.getSequence(master.pk()) == 1);
|
||||
|
||||
// A key with no manifest on-ledger resolves to nothing and leaves the
|
||||
// cache untouched.
|
||||
BEAST_EXPECT(
|
||||
!cache.applyLedgerSigningKey(*env.closed(), stranger.pk()));
|
||||
BEAST_EXPECT(cache.getMasterKey(stranger.pk()) == stranger.pk());
|
||||
|
||||
// A rotation is recovered from the new ephemeral key alone, and the
|
||||
// superseded key stops resolving because its object is gone.
|
||||
submit(env, makeManifest(master, eph2, 2));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
cache.applyLedgerSigningKey(*env.closed(), eph2.pk()) ==
|
||||
master.pk());
|
||||
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph2.pk());
|
||||
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(eph1.pk())));
|
||||
BEAST_EXPECT(!cache.applyLedgerSigningKey(*env.closed(), eph1.pk()));
|
||||
|
||||
// Asking again is answered from the cache, ahead of the per-ledger
|
||||
// probe bookkeeping.
|
||||
BEAST_EXPECT(
|
||||
cache.applyLedgerSigningKey(*env.closed(), eph2.pk()) ==
|
||||
master.pk());
|
||||
|
||||
// A master key is not a signing key. The object at its keylet is a
|
||||
// perfectly good manifest and is ingested, but it binds eph2, not the
|
||||
// master key, so nothing is reported for the key asked about.
|
||||
BEAST_EXPECT(!cache.applyLedgerSigningKey(*env.closed(), master.pk()));
|
||||
|
||||
// A revoked master publishes no ephemeral object at all, so this
|
||||
// direction goes quiet. The revocation still reaches the cache by
|
||||
// master key, which is what applyLedger() is for.
|
||||
submit(
|
||||
env,
|
||||
makeManifest(
|
||||
master, eph2, std::numeric_limits<std::uint32_t>::max()));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(eph2.pk())));
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
|
||||
BEAST_EXPECT(cache.revoked(master.pk()));
|
||||
}
|
||||
|
||||
void
|
||||
testMalformed(FeatureBitset features)
|
||||
{
|
||||
testcase("malformed submissions");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const other = Account("other", KeyType::ed25519);
|
||||
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master, other);
|
||||
env.close();
|
||||
|
||||
// Not a manifest at all.
|
||||
BEAST_EXPECT(
|
||||
submit(env, "not a manifest")[jss::error].asString() ==
|
||||
"invalidManifest");
|
||||
|
||||
// A manifest whose ephemeral signature does not check out.
|
||||
{
|
||||
auto blob = makeManifest(master, ephemeral, 1);
|
||||
blob[blob.size() - 1] ^= 0xFF;
|
||||
BEAST_EXPECT(
|
||||
submit(env, blob)[jss::error].asString() == "invalidManifest");
|
||||
}
|
||||
|
||||
// The master key's account must exist: it pays the fee and holds the
|
||||
// pointer to the manifest.
|
||||
auto const unfunded = Account("unfunded", KeyType::ed25519);
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(unfunded, ephemeral, 1))) ==
|
||||
"terNO_ACCOUNT");
|
||||
|
||||
// An ephemeral key already claimed by a different account would
|
||||
// collide with -- and clobber -- that account's manifest object.
|
||||
submit(env, makeManifest(master, ephemeral, 1));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(other, ephemeral, 1))) ==
|
||||
"tecDUPLICATE");
|
||||
}
|
||||
|
||||
void
|
||||
testEnvelopeRejections(FeatureBitset features)
|
||||
{
|
||||
testcase("envelope rejections");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const other = Account("other", KeyType::ed25519);
|
||||
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master, other);
|
||||
env.close();
|
||||
|
||||
auto const good = makeManifest(master, ephemeral, 1);
|
||||
|
||||
// Sanity check: the unmodified envelope is the one Submit builds, so
|
||||
// every rejection below is attributable to the tweak and nothing else.
|
||||
BEAST_EXPECT(
|
||||
applyDirect(env, envelope(env, good, master.id())) == tesSUCCESS);
|
||||
|
||||
// Any bit but tfFullyCanonicalSig (0x80000000), which is tfUniversal
|
||||
// and so permitted on every transaction.
|
||||
BEAST_EXPECT(
|
||||
applyDirect(
|
||||
env, envelope(env, good, master.id(), [](STObject& obj) {
|
||||
obj.setFieldU32(sfFlags, 0x00000001);
|
||||
})) == temINVALID_FLAG);
|
||||
|
||||
// sfManifest parses as an object but not as a manifest.
|
||||
BEAST_EXPECT(
|
||||
applyDirect(
|
||||
env, envelope(env, makeUnparseableManifest(), master.id())) ==
|
||||
temMALFORMED);
|
||||
|
||||
// A manifest whose signatures do not check out. checkValidity() would
|
||||
// stop this before preflight, so only a direct apply reaches the
|
||||
// transactor's own verify() call.
|
||||
{
|
||||
auto bad = good;
|
||||
bad[bad.size() - 1] ^= 0xFF;
|
||||
BEAST_EXPECT(
|
||||
applyDirect(env, envelope(env, bad, master.id())) ==
|
||||
temMALFORMED);
|
||||
}
|
||||
|
||||
// The envelope's account must be the manifest's master key.
|
||||
BEAST_EXPECT(
|
||||
applyDirect(env, envelope(env, good, other.id())) == temMALFORMED);
|
||||
|
||||
// Every envelope field a relayer could otherwise choose is pinned.
|
||||
for (auto const& [name, tweak] : std::vector<
|
||||
std::pair<char const*, std::function<void(STObject&)>>>{
|
||||
{"Sequence",
|
||||
[](STObject& obj) { obj.setFieldU32(sfSequence, 1); }},
|
||||
{"AccountTxnID",
|
||||
[](STObject& obj) {
|
||||
obj.setFieldH256(sfAccountTxnID, uint256{1});
|
||||
}},
|
||||
{"TicketSequence",
|
||||
[](STObject& obj) { obj.setFieldU32(sfTicketSequence, 1); }},
|
||||
{"SigningPubKey", [&](STObject& obj) {
|
||||
obj.setFieldVL(sfSigningPubKey, master.pk().slice());
|
||||
}}})
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
applyDirect(env, envelope(env, good, master.id(), tweak)) ==
|
||||
temMALFORMED,
|
||||
name);
|
||||
}
|
||||
|
||||
// sfFee is the one envelope field preflight cannot bound, because the
|
||||
// base fee is not in scope until preclaim. checkFee() caps it instead.
|
||||
auto const priced = envelope(env, good, master.id());
|
||||
auto const ceiling = priced->getFieldAmount(sfFee).xrp();
|
||||
|
||||
BEAST_EXPECT(
|
||||
applyDirect(
|
||||
env, envelope(env, good, master.id(), [&](STObject& obj) {
|
||||
obj.setFieldAmount(sfFee, ceiling + XRPAmount{1});
|
||||
})) == temBAD_FEE);
|
||||
|
||||
// At the ceiling exactly, which is what Submit sends.
|
||||
BEAST_EXPECT(
|
||||
applyDirect(
|
||||
env, envelope(env, good, master.id(), [&](STObject& obj) {
|
||||
obj.setFieldAmount(sfFee, ceiling);
|
||||
})) == tesSUCCESS);
|
||||
}
|
||||
|
||||
void
|
||||
testCorruptLedger(FeatureBitset features)
|
||||
{
|
||||
testcase("corrupt manifest state");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const eph1 = Account("eph1", KeyType::ed25519);
|
||||
auto const eph2 = Account("eph2", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, eph1, 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
auto const update =
|
||||
envelope(env, makeManifest(master, eph2, 2), master.id());
|
||||
|
||||
// sfManifestID on the account root points at nothing.
|
||||
BEAST_EXPECT(applyDirect(env, update, [&](OpenView& view) {
|
||||
rawErase(view, keylet::manifest(master.pk()));
|
||||
}) == tefBAD_LEDGER);
|
||||
|
||||
// The master copy survives but the ephemeral copy it points at is
|
||||
// gone.
|
||||
BEAST_EXPECT(applyDirect(env, update, [&](OpenView& view) {
|
||||
rawErase(view, keylet::manifest(eph1.pk()));
|
||||
}) == tefBAD_LEDGER);
|
||||
|
||||
// The reverse: the account root has forgotten its manifest, so the
|
||||
// erase pass is skipped and the keylet is found occupied.
|
||||
BEAST_EXPECT(applyDirect(env, update, [&](OpenView& view) {
|
||||
auto replacement =
|
||||
rawCopy(view.read(keylet::account(master.id())));
|
||||
replacement->makeFieldAbsent(sfManifestID);
|
||||
view.rawReplace(replacement);
|
||||
}) == tefBAD_LEDGER);
|
||||
|
||||
// None of the above was retained, so the ledger is still intact and an
|
||||
// ordinary update still works.
|
||||
BEAST_EXPECT(applyDirect(env, update) == tesSUCCESS);
|
||||
}
|
||||
|
||||
void
|
||||
testGossipSelection(FeatureBitset features)
|
||||
{
|
||||
testcase("gossip selection");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
std::vector<Account> masters;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
masters.emplace_back("gm" + std::to_string(i), KeyType::ed25519);
|
||||
|
||||
auto const ephemeral = Account("gossipeph", KeyType::ed25519);
|
||||
|
||||
for (auto const& m : masters)
|
||||
env.fund(XRP(1000), m);
|
||||
env.close();
|
||||
|
||||
// Each master needs its own ephemeral key: preclaim rejects a key
|
||||
// already claimed by another account.
|
||||
std::vector<Account> ephs;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
ephs.emplace_back("ge" + std::to_string(i), KeyType::ed25519);
|
||||
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[i], ephs[i], 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
}
|
||||
|
||||
ManifestCache cache;
|
||||
|
||||
hash_set<PublicKey> all;
|
||||
for (auto const& m : masters)
|
||||
all.insert(m.pk());
|
||||
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed(), all) == 4);
|
||||
|
||||
// The raw form is what a republishing node needs: the exact bytes the
|
||||
// master key signed, plus the sequence to compare against the ledger.
|
||||
auto const raw = cache.getRawManifest(masters[0].pk());
|
||||
if (BEAST_EXPECT(raw))
|
||||
{
|
||||
BEAST_EXPECT(raw->first == 1);
|
||||
BEAST_EXPECT(!raw->second.empty());
|
||||
}
|
||||
BEAST_EXPECT(!cache.getRawManifest(ephemeral.pk()));
|
||||
|
||||
// Pinning bumps the sequence so a cached gossip message is rebuilt,
|
||||
// and pinning the same set again does not.
|
||||
auto const seq = cache.sequence();
|
||||
cache.pin({masters[0].pk(), masters[1].pk()});
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
|
||||
auto const seq2 = cache.sequence();
|
||||
cache.pin({masters[0].pk(), masters[1].pk()});
|
||||
BEAST_EXPECT(cache.sequence() == seq2);
|
||||
|
||||
// Everything is offered: two pinned plus two under the gossip limit.
|
||||
std::size_t reserved = 0;
|
||||
std::vector<PublicKey> offered;
|
||||
cache.for_each_gossip_manifest(
|
||||
[&](std::size_t n) { reserved = n; },
|
||||
[&](Manifest const& m) { offered.push_back(m.masterKey); });
|
||||
|
||||
BEAST_EXPECT(reserved == 4);
|
||||
BEAST_EXPECT(offered.size() == 4);
|
||||
BEAST_EXPECT(
|
||||
hash_set<PublicKey>(offered.begin(), offered.end()) == all);
|
||||
|
||||
// A pinned key with no manifest is counted in the reservation but not
|
||||
// offered, since the reservation is only an upper bound.
|
||||
cache.pin({masters[0].pk(), ephemeral.pk()});
|
||||
offered.clear();
|
||||
cache.for_each_gossip_manifest(
|
||||
[&](std::size_t n) { reserved = n; },
|
||||
[&](Manifest const& m) { offered.push_back(m.masterKey); });
|
||||
|
||||
BEAST_EXPECT(reserved == 5);
|
||||
BEAST_EXPECT(offered.size() == 4);
|
||||
}
|
||||
|
||||
void
|
||||
testDisabled(FeatureBitset features)
|
||||
{
|
||||
testcase("amendment gate");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features - featureOnChainManifests};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master);
|
||||
env.close();
|
||||
|
||||
// The submitted transaction carries no account signature, so with the
|
||||
// amendment off it would otherwise be rejected as unsigned and never
|
||||
// reach preflight's temDISABLED. The RPC therefore refuses it up
|
||||
// front, and says why.
|
||||
auto const result = submit(env, makeManifest(master, ephemeral, 1));
|
||||
BEAST_EXPECT(result[jss::error].asString() == "notEnabled");
|
||||
BEAST_EXPECT(!result.isMember(jss::engine_result));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(master.pk())));
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
auto const sa = supported_amendments();
|
||||
testSubmission(sa);
|
||||
testUpdate(sa);
|
||||
testRevocation(sa);
|
||||
testRetrieval(sa);
|
||||
testSigningKeyRetrieval(sa);
|
||||
testMalformed(sa);
|
||||
testEnvelopeRejections(sa);
|
||||
testCorruptLedger(sa);
|
||||
testGossipSelection(sa);
|
||||
testDisabled(sa);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(SetManifest, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -24,6 +24,8 @@
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/rpc/GRPCTestClientBase.h>
|
||||
#include <xrpld/rpc/GRPCHandlers.h>
|
||||
#include <xrpl/hook/Misc.h>
|
||||
#include <xrpl/protocol/InnerObjectFormats.h>
|
||||
#include <xrpl/resource/Charge.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
@@ -121,6 +123,252 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testHooksRequest()
|
||||
{
|
||||
testcase("Hooks request");
|
||||
using namespace jtx;
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
|
||||
auto request = [&](Json::Value const& hooks, unsigned apiVersion) {
|
||||
Json::Value params;
|
||||
params[jss::account] = alice.human();
|
||||
params[jss::hooks] = hooks;
|
||||
if (apiVersion == 1)
|
||||
return env.rpc("json", "account_info", to_string(params));
|
||||
|
||||
params[jss::api_version] = 2;
|
||||
return env.rpc("json", "account_info", to_string(params));
|
||||
};
|
||||
|
||||
for (Json::Value const& invalid :
|
||||
{Json::Value(1),
|
||||
Json::Value(1.1),
|
||||
Json::Value("true"),
|
||||
Json::Value(Json::nullValue),
|
||||
Json::Value(Json::objectValue),
|
||||
Json::Value(Json::arrayValue)})
|
||||
{
|
||||
for (unsigned const apiVersion : {1u, 2u})
|
||||
{
|
||||
auto const info = request(invalid, apiVersion);
|
||||
BEAST_EXPECT(info[jss::result][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(
|
||||
info[jss::result][jss::error_message] ==
|
||||
"Invalid field 'hooks'.");
|
||||
}
|
||||
}
|
||||
|
||||
auto const withoutHooks = env.rpc(
|
||||
"json",
|
||||
"account_info",
|
||||
R"({"account": ")" + alice.human() + R"("})");
|
||||
BEAST_EXPECT(!withoutHooks[jss::result].isMember(jss::hooks));
|
||||
|
||||
auto const explicitlyFalse = env.rpc(
|
||||
"json",
|
||||
"account_info",
|
||||
R"({"account": ")" + alice.human() + R"(","hooks":false})");
|
||||
BEAST_EXPECT(!explicitlyFalse[jss::result].isMember(jss::hooks));
|
||||
|
||||
auto const explicitlyTrue = env.rpc(
|
||||
"json",
|
||||
"account_info",
|
||||
std::string{"{ "} + "\"account\": \"" + alice.human() +
|
||||
"\", \"hooks\": true }");
|
||||
BEAST_EXPECT(explicitlyTrue[jss::result][jss::hooks].isArray());
|
||||
BEAST_EXPECT(explicitlyTrue[jss::result][jss::hooks].size() == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testHooksEffectiveValues()
|
||||
{
|
||||
testcase("Effective hook values");
|
||||
|
||||
auto const* hookTemplate =
|
||||
InnerObjectFormats::getInstance().findSOTemplateBySField(sfHook);
|
||||
if (!BEAST_EXPECTS(hookTemplate != nullptr, "sfHook template"))
|
||||
return;
|
||||
// If this count changes, add or remove tests for the corresponding Hook
|
||||
// fields.
|
||||
BEAST_EXPECT(hookTemplate->size() == 12);
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
|
||||
uint256 const definitionNamespace{1};
|
||||
uint256 const accountNamespace{2};
|
||||
auto const makeParameter = [](std::string const& name,
|
||||
std::string const& value) {
|
||||
Json::Value parameter;
|
||||
parameter[jss::HookParameter][jss::HookParameterName] = name;
|
||||
parameter[jss::HookParameter][jss::HookParameterValue] = value;
|
||||
return parameter;
|
||||
};
|
||||
|
||||
auto create = hso(genesis::AcceptHook);
|
||||
create[jss::Flags] = hsfCOLLECT;
|
||||
create[jss::HookName] = strHex(std::string{"ACCT"});
|
||||
create[jss::HookNamespace] = to_string(definitionNamespace);
|
||||
create[jss::HookParameters] = Json::arrayValue;
|
||||
create[jss::HookParameters].append(makeParameter("AA", "BB"));
|
||||
create[jss::HookParameters].append(makeParameter("CC", "DD"));
|
||||
env(hook(alice, {{create, Json::Value{}}}, 0),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const hookSLE = env.le(keylet::hook(alice.id()));
|
||||
BEAST_EXPECT(hookSLE && hookSLE->isFieldPresent(sfHooks));
|
||||
if (!hookSLE || !hookSLE->isFieldPresent(sfHooks))
|
||||
return;
|
||||
auto const& installed = hookSLE->getFieldArray(sfHooks)[0];
|
||||
BEAST_EXPECT(installed.isFieldPresent(sfHookHash));
|
||||
if (!installed.isFieldPresent(sfHookHash))
|
||||
return;
|
||||
auto const hash = installed.getFieldH256(sfHookHash);
|
||||
|
||||
auto const hookDefinition = env.le(keylet::hookDefinition(hash));
|
||||
BEAST_EXPECT(hookDefinition);
|
||||
if (hookDefinition)
|
||||
{
|
||||
BEAST_EXPECT(!hookDefinition->isFieldPresent(sfHookName));
|
||||
BEAST_EXPECT(!hookDefinition->isFieldPresent(sfHookGrants));
|
||||
BEAST_EXPECT(hookDefinition->isFieldPresent(sfHookApiVersion));
|
||||
BEAST_EXPECT(hookDefinition->getFieldU16(sfHookApiVersion) == 0);
|
||||
}
|
||||
|
||||
auto accountInfo = env.rpc(
|
||||
"json",
|
||||
"account_info",
|
||||
R"({"account": ")" + alice.human() + R"(","hooks":true})");
|
||||
auto const& hookJson =
|
||||
accountInfo[jss::result][jss::hooks][0U][sfHook.jsonName];
|
||||
BEAST_EXPECT(hookJson[sfHookHash.jsonName] == to_string(hash));
|
||||
BEAST_EXPECT(!hookJson.isMember(sfCreateCode.jsonName));
|
||||
BEAST_EXPECT(
|
||||
hookJson[sfHookNamespace.jsonName] ==
|
||||
to_string(definitionNamespace));
|
||||
BEAST_EXPECT(
|
||||
hookJson[sfHookName.jsonName].isString() &&
|
||||
hookJson[sfHookName.jsonName] == strHex(std::string{"ACCT"}));
|
||||
BEAST_EXPECT(
|
||||
hookJson[sfHookApiVersion.jsonName].isInt() &&
|
||||
hookJson[sfHookApiVersion.jsonName].asUInt() == 0);
|
||||
BEAST_EXPECT(hookJson[sfHookOn.jsonName] == to_string(uint256{0}));
|
||||
BEAST_EXPECT(!hookJson.isMember(sfHookOnIncoming.jsonName));
|
||||
BEAST_EXPECT(!hookJson.isMember(sfHookOnOutgoing.jsonName));
|
||||
BEAST_EXPECT(
|
||||
hookJson[sfHookCanEmit.jsonName] ==
|
||||
to_string(UINT256_BIT[ttHOOK_SET]));
|
||||
BEAST_EXPECT(hookJson[sfFlags.jsonName] == hsfCOLLECT);
|
||||
BEAST_EXPECT(hookJson[sfHookParameters.jsonName].size() == 2);
|
||||
auto const& hooksJson = accountInfo[jss::result][jss::hooks];
|
||||
BEAST_EXPECT(hooksJson.isArray());
|
||||
BEAST_EXPECT(hooksJson.size() == 2);
|
||||
if (!hooksJson.isArray() || hooksJson.size() < 2)
|
||||
return;
|
||||
BEAST_EXPECT(hooksJson[1U].isObject());
|
||||
BEAST_EXPECT(hooksJson[1U].isMember(sfHook.jsonName));
|
||||
if (!hooksJson[1U].isObject() ||
|
||||
!hooksJson[1U].isMember(sfHook.jsonName))
|
||||
return;
|
||||
auto const& blankHook = hooksJson[1U][sfHook.jsonName];
|
||||
BEAST_EXPECT(blankHook.isObject());
|
||||
BEAST_EXPECT(blankHook.size() == 0);
|
||||
|
||||
Json::Value update;
|
||||
update[jss::HookHash] = to_string(hash);
|
||||
update[jss::Flags] = hsfOVERRIDE;
|
||||
update[jss::HookName] = strHex(std::string{"ACCT"});
|
||||
update[jss::HookGrants] = Json::arrayValue;
|
||||
update[jss::HookGrants][0U][jss::HookGrant][jss::HookHash] =
|
||||
to_string(hash);
|
||||
update[jss::HookNamespace] = to_string(accountNamespace);
|
||||
update[jss::HookOnIncoming] = to_string(uint256{5});
|
||||
update[jss::HookOnOutgoing] = to_string(uint256{6});
|
||||
update[jss::HookCanEmit] = to_string(uint256{4});
|
||||
update[jss::HookParameters] = Json::arrayValue;
|
||||
update[jss::HookParameters].append(makeParameter("AA", ""));
|
||||
update[jss::HookParameters].append(makeParameter("EE", "FF"));
|
||||
env(hook(alice, {{update}}, 0), fee(XRP(1)), ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
accountInfo = env.rpc(
|
||||
"json",
|
||||
"account_info",
|
||||
R"({"account": ")" + alice.human() + R"(","hooks":true})");
|
||||
auto const& overridden =
|
||||
accountInfo[jss::result][jss::hooks][0U][sfHook.jsonName];
|
||||
BEAST_EXPECT(
|
||||
overridden[sfHookNamespace.jsonName] ==
|
||||
to_string(accountNamespace));
|
||||
BEAST_EXPECT(
|
||||
overridden[sfHookName.jsonName].isString() &&
|
||||
overridden[sfHookName.jsonName] == strHex(std::string{"ACCT"}));
|
||||
BEAST_EXPECT(
|
||||
overridden[sfHookApiVersion.jsonName].isInt() &&
|
||||
overridden[sfHookApiVersion.jsonName].asUInt() == 0);
|
||||
BEAST_EXPECT(overridden[sfHookGrants.jsonName].isArray());
|
||||
BEAST_EXPECT(overridden[sfHookGrants.jsonName].size() == 1);
|
||||
if (overridden[sfHookGrants.jsonName].isArray() &&
|
||||
overridden[sfHookGrants.jsonName].size() == 1)
|
||||
{
|
||||
auto const& grant = overridden[sfHookGrants.jsonName][0U];
|
||||
BEAST_EXPECT(grant.isObject());
|
||||
BEAST_EXPECT(grant.size() == 1);
|
||||
BEAST_EXPECT(grant.isMember(sfHookGrant.jsonName));
|
||||
if (grant.isObject() && grant.size() == 1 &&
|
||||
grant.isMember(sfHookGrant.jsonName))
|
||||
{
|
||||
auto const& grantObject = grant[sfHookGrant.jsonName];
|
||||
BEAST_EXPECT(grantObject.isObject());
|
||||
BEAST_EXPECT(grantObject.size() == 1);
|
||||
BEAST_EXPECT(grantObject.isMember(sfHookHash.jsonName));
|
||||
if (grantObject.isObject() && grantObject.size() == 1 &&
|
||||
grantObject.isMember(sfHookHash.jsonName))
|
||||
BEAST_EXPECT(
|
||||
grantObject[sfHookHash.jsonName] == to_string(hash));
|
||||
}
|
||||
}
|
||||
BEAST_EXPECT(overridden[sfFlags.jsonName] == 0);
|
||||
BEAST_EXPECT(!overridden.isMember(sfHookOn.jsonName));
|
||||
BEAST_EXPECT(
|
||||
overridden[sfHookOnIncoming.jsonName] == to_string(uint256{5}));
|
||||
BEAST_EXPECT(
|
||||
overridden[sfHookOnOutgoing.jsonName] == to_string(uint256{6}));
|
||||
BEAST_EXPECT(
|
||||
overridden[sfHookCanEmit.jsonName] == to_string(uint256{4}));
|
||||
BEAST_EXPECT(overridden[sfHookParameters.jsonName].size() == 3);
|
||||
if (overridden[sfHookParameters.jsonName].size() != 3)
|
||||
return;
|
||||
auto expectParameter = [&](std::string const& name,
|
||||
std::string const& value) {
|
||||
bool found = false;
|
||||
for (auto const& parameter : overridden[sfHookParameters.jsonName])
|
||||
{
|
||||
if (!parameter.isObject() ||
|
||||
!parameter.isMember(sfHookParameter.jsonName))
|
||||
continue;
|
||||
auto const& object = parameter[sfHookParameter.jsonName];
|
||||
if (!object.isObject() ||
|
||||
object[sfHookParameterName.jsonName] != name)
|
||||
continue;
|
||||
found = true;
|
||||
BEAST_EXPECT(object[sfHookParameterValue.jsonName] == value);
|
||||
break;
|
||||
}
|
||||
BEAST_EXPECT(found);
|
||||
};
|
||||
expectParameter("AA", "");
|
||||
expectParameter("CC", "DD");
|
||||
expectParameter("EE", "FF");
|
||||
}
|
||||
|
||||
// Test the "signer_lists" argument in account_info.
|
||||
void
|
||||
testSignerLists()
|
||||
@@ -686,6 +934,8 @@ public:
|
||||
run() override
|
||||
{
|
||||
testErrors();
|
||||
testHooksRequest();
|
||||
testHooksEffectiveValues();
|
||||
testSignerLists();
|
||||
testSignerListsApiVersion2();
|
||||
testSignerListsV2();
|
||||
|
||||
@@ -19,15 +19,548 @@
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <xrpld/app/consensus/RCLValidations.h>
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/AmendmentTable.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/temp_dir.h>
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class AmendmentBlocked_test : public beast::unit_test::suite
|
||||
{
|
||||
// An amendment id this binary will never support.
|
||||
static uint256
|
||||
unsupportedAmendmentId()
|
||||
{
|
||||
std::string const in = "AmendmentBlocked_test.unsupported";
|
||||
sha256_hasher h;
|
||||
using beast::hash_append;
|
||||
hash_append(h, in);
|
||||
auto const d = static_cast<sha256_hasher::result_type>(h);
|
||||
uint256 result;
|
||||
std::memcpy(result.data(), d.data(), d.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
// The majority period only sets how far out activation is expected, so
|
||||
// shorten it from two weeks. That keeps the ledger close-time jumps in
|
||||
// these tests down to minutes, and makes the relationship to the
|
||||
// five-minute shutdown lead time in LedgerMaster obvious.
|
||||
static std::unique_ptr<Config>
|
||||
shortMajorityConfig()
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
auto cfg = test::jtx::envconfig();
|
||||
cfg->AMENDMENT_MAJORITY_TIME = 15min;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Give the amendment table a majority, as of now, for an amendment we do
|
||||
// not support; activation is then expected one majority period out.
|
||||
// Bypasses the ReadView overload (and therefore needValidatedLedger) on
|
||||
// purpose: because lastUpdateSeq_ is set to the current sequence, later
|
||||
// closes within the same 256-ledger block will not recompute -- and so
|
||||
// will not clear -- what we inject here.
|
||||
void
|
||||
injectUnsupportedMajority(test::jtx::Env& env)
|
||||
{
|
||||
auto const seq = env.closed()->info().seq;
|
||||
majorityAmendments_t majority;
|
||||
majority[unsupportedAmendmentId()] = env.now();
|
||||
env.app().getAmendmentTable().doValidatedLedger(seq, {}, majority);
|
||||
|
||||
auto const first =
|
||||
env.app().getAmendmentTable().firstUnsupportedExpected();
|
||||
BEAST_EXPECT(
|
||||
first &&
|
||||
*first == env.now() + env.app().config().AMENDMENT_MAJORITY_TIME);
|
||||
}
|
||||
|
||||
void
|
||||
testReceiptFileHelpers()
|
||||
{
|
||||
testcase("amendment blocked receipt helpers");
|
||||
|
||||
auto const id = unsupportedAmendmentId();
|
||||
|
||||
{
|
||||
beast::temp_dir td;
|
||||
Config cfg;
|
||||
cfg.CONFIG_DIR = td.path();
|
||||
|
||||
auto const path = amendmentBlockedFilePath(cfg);
|
||||
BEAST_EXPECT(path.filename() == "README_AMENDMENT_BLOCKED");
|
||||
BEAST_EXPECT(
|
||||
path.parent_path() == boost::filesystem::path{td.path()});
|
||||
BEAST_EXPECT(!boost::filesystem::exists(path));
|
||||
|
||||
// Nothing to remove yet, and that is not an error.
|
||||
boost::system::error_code ec;
|
||||
BEAST_EXPECT(!removeAmendmentBlockedFile(cfg, ec));
|
||||
BEAST_EXPECT(!ec);
|
||||
|
||||
BEAST_EXPECT(!writeAmendmentBlockedFile(
|
||||
cfg, {to_string(id) + " (already active)"}));
|
||||
BEAST_EXPECT(boost::filesystem::exists(path));
|
||||
|
||||
auto const contents = getFileContents(ec, path);
|
||||
BEAST_EXPECT(!ec);
|
||||
BEAST_EXPECT(
|
||||
contents.find("XAHAUD STOPPED: UPGRADE REQUIRED") == 0);
|
||||
// When it stopped, what was running, and what it choked on.
|
||||
BEAST_EXPECT(contents.find("Stopped at:") != std::string::npos);
|
||||
BEAST_EXPECT(
|
||||
contents.find(BuildInfo::getVersionString()) !=
|
||||
std::string::npos);
|
||||
BEAST_EXPECT(contents.find(to_string(id)) != std::string::npos);
|
||||
BEAST_EXPECT(contents.find("already active") != std::string::npos);
|
||||
BEAST_EXPECT(contents.find("Upgrade xahaud") != std::string::npos);
|
||||
// The receipt is self-clearing, so it must not tell the operator
|
||||
// to delete anything.
|
||||
BEAST_EXPECT(
|
||||
contents.find("removed automatically") != std::string::npos);
|
||||
|
||||
// The timestamp is rendered, not a placeholder: to_string_iso
|
||||
// gives YYYY-MM-DDTHH:MM:SSZ.
|
||||
auto const stampAt = contents.find("Stopped at:");
|
||||
if (BEAST_EXPECT(stampAt != std::string::npos))
|
||||
{
|
||||
auto const eol = contents.find('\n', stampAt);
|
||||
auto const line = contents.substr(stampAt, eol - stampAt);
|
||||
BEAST_EXPECT(line.find("20") != std::string::npos);
|
||||
BEAST_EXPECT(line.find('T') != std::string::npos);
|
||||
BEAST_EXPECT(line.find('Z') != std::string::npos);
|
||||
}
|
||||
|
||||
// Now it can be removed, and removal is reported.
|
||||
ec.clear();
|
||||
BEAST_EXPECT(removeAmendmentBlockedFile(cfg, ec));
|
||||
BEAST_EXPECT(!ec);
|
||||
BEAST_EXPECT(!boost::filesystem::exists(path));
|
||||
|
||||
// With no amendments to name the receipt still says something
|
||||
// useful.
|
||||
BEAST_EXPECT(!writeAmendmentBlockedFile(cfg, {}));
|
||||
ec.clear();
|
||||
auto const bare = getFileContents(ec, path);
|
||||
BEAST_EXPECT(!ec);
|
||||
BEAST_EXPECT(
|
||||
bare.find("not supported by this build") != std::string::npos);
|
||||
}
|
||||
|
||||
// A directory we cannot write to must surface an error rather than
|
||||
// throw. The shutdown continues either way; only the receipt is lost,
|
||||
// which is what happens on installs where CONFIG_DIR is read-only for
|
||||
// the account xahaud runs as.
|
||||
{
|
||||
Config cfg;
|
||||
cfg.CONFIG_DIR =
|
||||
boost::filesystem::path{"/"} / "no" / "such" / "directory";
|
||||
BEAST_EXPECT(!!writeAmendmentBlockedFile(cfg, {}));
|
||||
|
||||
boost::system::error_code ec;
|
||||
BEAST_EXPECT(!removeAmendmentBlockedFile(cfg, ec));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testStandaloneDoesNotStop()
|
||||
{
|
||||
testcase("standalone does not stop or leave a receipt");
|
||||
using namespace test::jtx;
|
||||
|
||||
beast::temp_dir td;
|
||||
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
|
||||
cfg->CONFIG_DIR = td.path();
|
||||
return cfg;
|
||||
})};
|
||||
BEAST_EXPECT(env.app().config().standalone());
|
||||
|
||||
auto const path = amendmentBlockedFilePath(env.app().config());
|
||||
env.app().getOPs().setAmendmentBlocked();
|
||||
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
|
||||
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
|
||||
BEAST_EXPECT(!env.app().isStopping());
|
||||
BEAST_EXPECT(!boost::filesystem::exists(path));
|
||||
}
|
||||
|
||||
void
|
||||
testShutdownOnBlock()
|
||||
{
|
||||
testcase("amendment blocked stops the server");
|
||||
using namespace test::jtx;
|
||||
|
||||
// A non-standalone Env is needed to exercise shutdown rather than
|
||||
// only setting the blocked flag. Consequences when editing:
|
||||
// setup() arms the state timer, run() arms the deadlock detector, and
|
||||
// signalStop() below releases run() to tear the application down
|
||||
// concurrently with the rest of this function. Do all the assertions
|
||||
// straight away and let the Env go out of scope promptly.
|
||||
beast::temp_dir td;
|
||||
Env env{*this, envconfig([&](std::unique_ptr<Config> config) {
|
||||
config->NODE_SIZE = 0;
|
||||
config->setupControl(true, true, false);
|
||||
// setupControl picks a production node size for
|
||||
// non-standalone; put it back to "tiny" for the test.
|
||||
config->NODE_SIZE = 0;
|
||||
config->CONFIG_DIR = td.path();
|
||||
config->legacy("database_path", td.path());
|
||||
return config;
|
||||
})};
|
||||
BEAST_EXPECT(!env.app().config().standalone());
|
||||
|
||||
auto const path = amendmentBlockedFilePath(env.app().config());
|
||||
BEAST_EXPECT(path.filename() == "README_AMENDMENT_BLOCKED");
|
||||
BEAST_EXPECT(!boost::filesystem::exists(path));
|
||||
BEAST_EXPECT(!env.app().isStopping());
|
||||
|
||||
// Make the table report an unsupported amendment so the receipt has
|
||||
// something concrete to name.
|
||||
auto const id = unsupportedAmendmentId();
|
||||
env.app().getAmendmentTable().enable(id);
|
||||
BEAST_EXPECT(env.app().getAmendmentTable().hasUnsupportedEnabled());
|
||||
|
||||
env.app().getOPs().setAmendmentBlocked();
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
|
||||
BEAST_EXPECT(env.app().isStopping());
|
||||
BEAST_EXPECT(boost::filesystem::exists(path));
|
||||
|
||||
boost::system::error_code readError;
|
||||
auto const contents = getFileContents(readError, path);
|
||||
BEAST_EXPECT(!readError);
|
||||
BEAST_EXPECT(
|
||||
contents.find("XAHAUD STOPPED: UPGRADE REQUIRED") !=
|
||||
std::string::npos);
|
||||
BEAST_EXPECT(contents.find("Upgrade xahaud") != std::string::npos);
|
||||
BEAST_EXPECT(contents.find(to_string(id)) != std::string::npos);
|
||||
BEAST_EXPECT(contents.find("already active") != std::string::npos);
|
||||
|
||||
// Repeat calls are a no-op: Change::applyAmendment reaches this from
|
||||
// the transaction apply path without an isBlocked() guard, so it must
|
||||
// not rewrite the receipt or re-log once per ledger.
|
||||
BEAST_EXPECT(boost::filesystem::remove(path));
|
||||
env.app().getOPs().setAmendmentBlocked();
|
||||
BEAST_EXPECT(!boost::filesystem::exists(path));
|
||||
}
|
||||
|
||||
void
|
||||
checkJump(bool unsupported)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
beast::temp_dir td;
|
||||
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
|
||||
cfg->setupControl(true, true, false);
|
||||
cfg->NODE_SIZE = 0;
|
||||
cfg->CONFIG_DIR = td.path();
|
||||
cfg->legacy("database_path", td.path());
|
||||
return cfg;
|
||||
})};
|
||||
auto& app = env.app();
|
||||
auto& ops = app.getOPs();
|
||||
auto& master = app.getLedgerMaster();
|
||||
std::lock_guard lock(app.getMasterMutex());
|
||||
auto const before = master.getClosedLedger();
|
||||
|
||||
// A direct child is not preferred over our LCL: consensus may be
|
||||
// about to build it. Two ledgers ahead forces the JUMP path.
|
||||
auto parent =
|
||||
std::make_shared<Ledger>(*before, app.timeKeeper().closeTime());
|
||||
parent->updateSkipList();
|
||||
parent->setImmutable();
|
||||
auto candidate =
|
||||
std::make_shared<Ledger>(*parent, app.timeKeeper().closeTime());
|
||||
candidate->updateSkipList();
|
||||
|
||||
if (unsupported)
|
||||
{
|
||||
auto const key = keylet::amendments();
|
||||
auto const existing = candidate->read(key);
|
||||
auto sle = existing ? std::make_shared<SLE>(*existing)
|
||||
: std::make_shared<SLE>(key);
|
||||
STVector256 amendments;
|
||||
if (sle->isFieldPresent(sfAmendments))
|
||||
amendments = sle->getFieldV256(sfAmendments);
|
||||
amendments.push_back(unsupportedAmendmentId());
|
||||
sle->setFieldV256(sfAmendments, amendments);
|
||||
if (existing)
|
||||
candidate->rawReplace(sle);
|
||||
else
|
||||
candidate->rawInsert(sle);
|
||||
}
|
||||
|
||||
// A serialized uint32 with an unknown field number. Insert raw bytes
|
||||
// so the real transaction parser, reached through TxQ, must throw.
|
||||
BEAST_EXPECT(SField::getField(STI_UINT32, 255).isInvalid());
|
||||
auto tx = std::make_shared<Serializer>();
|
||||
tx->add8(0x20);
|
||||
tx->add8(255);
|
||||
while (tx->getDataLength() < txMinSizeBytes)
|
||||
tx->add8(0);
|
||||
auto meta = std::make_shared<Serializer>();
|
||||
meta->add8(0xE1);
|
||||
auto const txID = sha512Half(tx->slice());
|
||||
candidate->rawTxInsert(txID, tx, meta);
|
||||
candidate->setImmutable();
|
||||
|
||||
bool unknownField = false;
|
||||
try
|
||||
{
|
||||
candidate->txRead(txID);
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
unknownField = std::string(e.what()).starts_with("Unknown field");
|
||||
}
|
||||
if (!BEAST_EXPECT(unknownField))
|
||||
return;
|
||||
|
||||
master.storeLedger(candidate);
|
||||
auto const keys = randomKeyPair(KeyType::secp256k1);
|
||||
auto const nodeID = calcNodeID(keys.first);
|
||||
auto validation = std::make_shared<STValidation>(
|
||||
app.timeKeeper().closeTime(),
|
||||
keys.first,
|
||||
keys.second,
|
||||
nodeID,
|
||||
[&](STValidation& v) {
|
||||
v.setFieldH256(sfLedgerHash, candidate->info().hash);
|
||||
v.setFieldU32(sfLedgerSequence, candidate->seq());
|
||||
});
|
||||
// Add directly so ledger acceptance does not discover the amendment
|
||||
// before JUMP has a chance to inspect the candidate itself.
|
||||
BEAST_EXPECT(
|
||||
app.getValidations().add(nodeID, RCLValidation{validation}) ==
|
||||
ValStatus::current);
|
||||
if (!BEAST_EXPECT(
|
||||
app.getValidations().getPreferredLCL(
|
||||
RCLValidatedLedger{before, env.journal},
|
||||
master.getValidLedgerIndex(),
|
||||
{}) == candidate->info().hash))
|
||||
return;
|
||||
|
||||
BEAST_EXPECT(!app.getAmendmentTable().hasUnsupportedEnabled());
|
||||
BEAST_EXPECT(!app.getAmendmentTable().firstUnsupportedExpected());
|
||||
BEAST_EXPECT(!ops.isAmendmentBlocked());
|
||||
BEAST_EXPECT(!app.isStopping());
|
||||
auto const receipt = amendmentBlockedFilePath(app.config());
|
||||
BEAST_EXPECT(!boost::filesystem::exists(receipt));
|
||||
|
||||
bool threw = false;
|
||||
try
|
||||
{
|
||||
ops.endConsensus({});
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
threw = true;
|
||||
BEAST_EXPECT(std::string(e.what()).starts_with("Unknown field"));
|
||||
}
|
||||
BEAST_EXPECT(threw == !unsupported);
|
||||
BEAST_EXPECT(ops.isAmendmentBlocked() == unsupported);
|
||||
BEAST_EXPECT(app.isStopping() == unsupported);
|
||||
BEAST_EXPECT(boost::filesystem::exists(receipt) == unsupported);
|
||||
BEAST_EXPECT(
|
||||
master.getClosedLedger()->info().hash == before->info().hash);
|
||||
}
|
||||
|
||||
void
|
||||
testJumpStopsForUnsupportedAmendment()
|
||||
{
|
||||
testcase("JUMP to an unsupported ledger stops the server");
|
||||
checkJump(/*unsupported=*/true);
|
||||
}
|
||||
|
||||
void
|
||||
testJumpRethrowsParsingError()
|
||||
{
|
||||
testcase("JUMP parsing errors without unsupported amendments escape");
|
||||
checkJump(/*unsupported=*/false);
|
||||
}
|
||||
|
||||
void
|
||||
testWarnsOutsideShutdownWindow()
|
||||
{
|
||||
testcase("unsupported majority warns while activation is distant");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, shortMajorityConfig()};
|
||||
BEAST_EXPECT(!env.app().getOPs().isBlocked());
|
||||
|
||||
BEAST_EXPECT(env.close());
|
||||
injectUnsupportedMajority(env);
|
||||
BEAST_EXPECT(env.close());
|
||||
|
||||
// A majority period out: warn, keep running.
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
|
||||
BEAST_EXPECT(!env.app().getOPs().isAmendmentBlocked());
|
||||
|
||||
// The table can name the amendment, and reports it as pending rather
|
||||
// than active. This is what ends up in the receipt.
|
||||
auto const unsupported =
|
||||
env.app().getAmendmentTable().unsupportedAmendments();
|
||||
BEAST_EXPECT(unsupported.size() == 1);
|
||||
if (unsupported.size() == 1)
|
||||
{
|
||||
BEAST_EXPECT(unsupported[0].id == unsupportedAmendmentId());
|
||||
BEAST_EXPECT(
|
||||
unsupported[0].expected ==
|
||||
env.app().getAmendmentTable().firstUnsupportedExpected());
|
||||
}
|
||||
|
||||
// Because firstUnsupportedExpected() is set, the warning carries the
|
||||
// expected activation date.
|
||||
auto const si = env.rpc("server_info")[jss::result];
|
||||
BEAST_EXPECT(si.isMember(jss::info));
|
||||
auto const& warnings = si[jss::info][jss::warnings];
|
||||
BEAST_EXPECT(warnings.isArray() && warnings.size() == 1);
|
||||
if (warnings.isArray() && warnings.size() == 1)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
warnings[0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY);
|
||||
auto const& details = warnings[0u][jss::details];
|
||||
BEAST_EXPECT(details.isMember(jss::expected_date));
|
||||
BEAST_EXPECT(details.isMember(jss::expected_date_UTC));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testUnsupportedAmendmentReporting()
|
||||
{
|
||||
testcase("unsupported amendments are reported for the receipt");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, shortMajorityConfig()};
|
||||
auto& table = env.app().getAmendmentTable();
|
||||
BEAST_EXPECT(table.unsupportedAmendments().empty());
|
||||
|
||||
// Majority, not yet active -> reported with an expected time.
|
||||
BEAST_EXPECT(env.close());
|
||||
injectUnsupportedMajority(env);
|
||||
auto pending = table.unsupportedAmendments();
|
||||
BEAST_EXPECT(pending.size() == 1);
|
||||
if (pending.size() == 1)
|
||||
BEAST_EXPECT(pending[0].expected.has_value());
|
||||
|
||||
// Majority lost -> nothing to report. The list is recomputed from the
|
||||
// ledger each time, so it must not accumulate.
|
||||
auto const seq = env.closed()->info().seq;
|
||||
table.doValidatedLedger(seq, {}, {});
|
||||
BEAST_EXPECT(table.unsupportedAmendments().empty());
|
||||
BEAST_EXPECT(!table.firstUnsupportedExpected());
|
||||
|
||||
// Active -> reported with no expected time.
|
||||
BEAST_EXPECT(table.enable(unsupportedAmendmentId()));
|
||||
BEAST_EXPECT(table.hasUnsupportedEnabled());
|
||||
auto const active = table.unsupportedAmendments();
|
||||
BEAST_EXPECT(active.size() == 1);
|
||||
if (active.size() == 1)
|
||||
{
|
||||
BEAST_EXPECT(active[0].id == unsupportedAmendmentId());
|
||||
BEAST_EXPECT(!active[0].expected);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWarningVisibleWithoutAdmin()
|
||||
{
|
||||
testcase("unsupported majority warning is not admin only");
|
||||
using namespace test::jtx;
|
||||
|
||||
// No closes here: ledger_accept is Role::ADMIN, server_info is
|
||||
// Role::USER, which is the whole point of the test.
|
||||
Env env{*this, envconfig(no_admin)};
|
||||
env.app().getOPs().setAmendmentWarned();
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
|
||||
|
||||
auto const si = env.rpc("server_info")[jss::result];
|
||||
BEAST_EXPECT(si.isMember(jss::info));
|
||||
auto const& warnings = si[jss::info][jss::warnings];
|
||||
BEAST_EXPECT(warnings.isArray() && warnings.size() == 1);
|
||||
if (warnings.isArray() && warnings.size() == 1)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
warnings[0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testShutsDownInsideShutdownWindow()
|
||||
{
|
||||
testcase("unsupported majority stops the server before activation");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, shortMajorityConfig()};
|
||||
BEAST_EXPECT(!env.app().getOPs().isBlocked());
|
||||
|
||||
BEAST_EXPECT(env.close());
|
||||
injectUnsupportedMajority(env);
|
||||
|
||||
// First close only warns: activation is still a majority period away.
|
||||
BEAST_EXPECT(env.close());
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
|
||||
BEAST_EXPECT(!env.app().getOPs().isAmendmentBlocked());
|
||||
|
||||
auto const expected =
|
||||
*env.app().getAmendmentTable().firstUnsupportedExpected();
|
||||
|
||||
// Close two minutes short of the expected activation time. This is not
|
||||
// a flag ledger and the warning has already been issued, so the check
|
||||
// has to run on every validated ledger to see this at all.
|
||||
BEAST_EXPECT(env.close(expected - NetClock::duration{120}));
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
|
||||
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
|
||||
|
||||
// Standalone, so the flag is set but the server keeps running.
|
||||
BEAST_EXPECT(!env.app().isStopping());
|
||||
}
|
||||
|
||||
void
|
||||
testShutsDownWhenActivationOverdue()
|
||||
{
|
||||
testcase("unsupported majority stops the server when overdue");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, shortMajorityConfig()};
|
||||
BEAST_EXPECT(!env.app().getOPs().isBlocked());
|
||||
|
||||
BEAST_EXPECT(env.close());
|
||||
injectUnsupportedMajority(env);
|
||||
|
||||
auto const expected =
|
||||
*env.app().getAmendmentTable().firstUnsupportedExpected();
|
||||
|
||||
// firstUnsupportedExpected() is a lower bound: the amendment actually
|
||||
// activates at the first flag ledger at or after it, so the server can
|
||||
// legitimately still be running an hour past it. That is the most
|
||||
// dangerous state, not the safest, and must stop the server.
|
||||
BEAST_EXPECT(env.close(expected + NetClock::duration{3600}));
|
||||
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
|
||||
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
|
||||
}
|
||||
|
||||
void
|
||||
testBlockedMethods()
|
||||
{
|
||||
@@ -250,6 +783,16 @@ public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testReceiptFileHelpers();
|
||||
testStandaloneDoesNotStop();
|
||||
testShutdownOnBlock();
|
||||
testJumpRethrowsParsingError();
|
||||
testJumpStopsForUnsupportedAmendment();
|
||||
testWarnsOutsideShutdownWindow();
|
||||
testUnsupportedAmendmentReporting();
|
||||
testWarningVisibleWithoutAdmin();
|
||||
testShutsDownInsideShutdownWindow();
|
||||
testShutsDownWhenActivationOverdue();
|
||||
testBlockedMethods();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
#include <xrpld/app/ledger/InboundLedger.h>
|
||||
#include <xrpld/app/ledger/InboundLedgers.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/consensus/LedgerTiming.h>
|
||||
@@ -31,6 +33,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -176,6 +179,25 @@ handleNewValidation(
|
||||
auto const& hash = val->getLedgerHash();
|
||||
auto const seq = val->getFieldU32(sfLedgerSequence);
|
||||
|
||||
// A validator that rotated its ephemeral key while this node was not
|
||||
// listening signs with a key no held manifest mentions, which is
|
||||
// indistinguishable from a validator this node has never heard of. Every
|
||||
// manifest published on-chain is written at its ephemeral keylet as well
|
||||
// as its master one, so that binding is recoverable in a single read.
|
||||
//
|
||||
// Done here rather than left to the next consensus round, because
|
||||
// ManifestCache::applyLedger() runs there against the master keys already
|
||||
// trusted and so cannot resolve a key whose master is what is missing.
|
||||
// Resolving now also means this validation is classified in the round it
|
||||
// arrived in rather than written off along with every other one until the
|
||||
// trusted set is next recomputed.
|
||||
if (app.validatorManifests().getMasterKey(signingKey) == signingKey)
|
||||
{
|
||||
if (auto const view = app.openLedger().current();
|
||||
view && view->rules().enabled(featureOnChainManifests))
|
||||
app.validatorManifests().applyLedgerSigningKey(*view, signingKey);
|
||||
}
|
||||
|
||||
// Ensure validation is marked as trusted if signer currently trusted
|
||||
auto masterKey = app.validators().getTrustedKey(signingKey);
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
#include <xrpld/app/tx/detail/NFTokenUtils.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/st.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
@@ -513,6 +515,26 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
case ttCRON_SET: {
|
||||
break;
|
||||
}
|
||||
case ttMANIFEST_SET: {
|
||||
// The ephemeral key's logical account, meaning the r-address its
|
||||
// public key hashes to, is a weak stake holder: the manifest names
|
||||
// that key but nothing is done to the account, so it may observe
|
||||
// but not rollback. Usually no such account exists, in which case
|
||||
// nothing executes.
|
||||
if (!tx.isFieldPresent(sfManifest))
|
||||
break;
|
||||
|
||||
STObject const& man =
|
||||
const_cast<STTx&>(tx).getField(sfManifest).downcast<STObject>();
|
||||
|
||||
if (!man.isFieldPresent(sfSigningPubKey))
|
||||
break;
|
||||
|
||||
auto const spk = man.getFieldVL(sfSigningPubKey);
|
||||
if (publicKeyType(makeSlice(spk)))
|
||||
ADD_TSH(calcAccountID(PublicKey(makeSlice(spk))), tshWEAK);
|
||||
break;
|
||||
}
|
||||
case ttAMM_CREATE:
|
||||
case ttAMM_DEPOSIT:
|
||||
case ttAMM_WITHDRAW:
|
||||
|
||||
@@ -289,14 +289,49 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
|
||||
app_.getSHAMapStore().onLedgerClosed(getValidatedLedger());
|
||||
mLedgerHistory.validatedLedger(l, consensusHash);
|
||||
app_.getAmendmentTable().doValidatedLedger(l);
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// How far ahead of the expected activation time to stop the server.
|
||||
// firstUnsupportedExpected() is only a lower bound on activation: the
|
||||
// amendment goes live at the first flag ledger at or after that time, so
|
||||
// stopping early is harmless while stopping late risks being handed a
|
||||
// ledger we cannot deserialize.
|
||||
constexpr auto amendmentShutdownLeadTime = 5min;
|
||||
|
||||
if (!app_.getOPs().isBlocked())
|
||||
{
|
||||
auto const firstUnsupported =
|
||||
app_.getAmendmentTable().firstUnsupportedExpected();
|
||||
|
||||
if (app_.getAmendmentTable().hasUnsupportedEnabled())
|
||||
{
|
||||
JLOG(m_journal.error()) << "One or more unsupported amendments "
|
||||
"activated: server blocked.";
|
||||
app_.getOPs().setAmendmentBlocked();
|
||||
}
|
||||
else if (
|
||||
firstUnsupported &&
|
||||
app_.timeKeeper().closeTime() + amendmentShutdownLeadTime >=
|
||||
*firstUnsupported)
|
||||
{
|
||||
// Activation is imminent, or the expected time has already passed
|
||||
// and we are only waiting on the next flag ledger. Shut down now,
|
||||
// while we can still deserialize the ledgers we are handed.
|
||||
//
|
||||
// This is deliberately checked on every validated ledger rather
|
||||
// than only on flag ledgers: the lead time above is much shorter
|
||||
// than the flag ledger interval, so a flag-ledger-only check would
|
||||
// usually skip straight over the window.
|
||||
//
|
||||
// The comparison must not be written as (*first - now), because
|
||||
// NetClock::rep is unsigned and wraps once the expected time is in
|
||||
// the past -- which is the most dangerous case, not the safest.
|
||||
JLOG(m_journal.error())
|
||||
<< "Unsupported amendment expected to activate at "
|
||||
<< to_string(*firstUnsupported) << ". Shutting down.";
|
||||
app_.getOPs().setAmendmentBlocked();
|
||||
}
|
||||
else if (!app_.getOPs().isAmendmentWarned() || l->isFlagLedger())
|
||||
{
|
||||
// Amendments can lose majority, so re-check periodically (every
|
||||
@@ -308,12 +343,11 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
|
||||
// this message may be logged more than once per session, because
|
||||
// the node will otherwise function normally, and this gives
|
||||
// operators an opportunity to see and resolve the warning.
|
||||
if (auto const first =
|
||||
app_.getAmendmentTable().firstUnsupportedExpected())
|
||||
if (firstUnsupported)
|
||||
{
|
||||
JLOG(m_journal.error()) << "One or more unsupported amendments "
|
||||
"reached majority. Upgrade before "
|
||||
<< to_string(*first)
|
||||
<< to_string(*firstUnsupported)
|
||||
<< " to prevent your server from "
|
||||
"becoming amendment blocked.";
|
||||
app_.getOPs().setAmendmentWarned();
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||
#include <xrpld/shamap/NodeFamily.h>
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/ResolverAsio.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
@@ -2372,6 +2373,65 @@ Application::Application() : beast::PropertyStream::Source("app")
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
boost::filesystem::path
|
||||
amendmentBlockedFilePath(Config const& config)
|
||||
{
|
||||
return config.CONFIG_DIR / "README_AMENDMENT_BLOCKED";
|
||||
}
|
||||
|
||||
boost::system::error_code
|
||||
writeAmendmentBlockedFile(
|
||||
Config const& config,
|
||||
std::vector<std::string> const& amendments)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
|
||||
std::ostringstream ss;
|
||||
ss << "XAHAUD STOPPED: UPGRADE REQUIRED\n"
|
||||
<< "\n"
|
||||
<< "Stopped at: "
|
||||
<< to_string_iso(time_point_cast<seconds>(system_clock::now())) << "\n"
|
||||
<< "This build: " << BuildInfo::getVersionString() << "\n"
|
||||
<< "\n";
|
||||
|
||||
if (amendments.empty())
|
||||
{
|
||||
ss << "One or more network amendments are not supported by this "
|
||||
"build.\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
ss << "Amendments this build does not support:\n";
|
||||
for (auto const& amendment : amendments)
|
||||
ss << " " << amendment << "\n";
|
||||
}
|
||||
|
||||
ss << "\n"
|
||||
<< "The network has moved to rules this build does not implement, so "
|
||||
"the\n"
|
||||
<< "server stopped rather than keep serving ledgers it cannot read.\n"
|
||||
<< "\n"
|
||||
<< "To get back in sync:\n"
|
||||
<< "1. Upgrade xahaud to a version that supports the amendments "
|
||||
"above.\n"
|
||||
<< "2. Start xahaud again. This file is removed automatically on "
|
||||
"start.\n"
|
||||
<< "\n"
|
||||
<< "Starting this build again without upgrading will stop the server "
|
||||
"again\n"
|
||||
<< "and rewrite this file. Nothing needs to be deleted by hand.\n";
|
||||
|
||||
boost::system::error_code ec;
|
||||
writeFileContents(ec, amendmentBlockedFilePath(config), ss.str());
|
||||
return ec;
|
||||
}
|
||||
|
||||
bool
|
||||
removeAmendmentBlockedFile(Config const& config, boost::system::error_code& ec)
|
||||
{
|
||||
return boost::filesystem::remove(amendmentBlockedFilePath(config), ec);
|
||||
}
|
||||
|
||||
std::unique_ptr<Application>
|
||||
make_Application(
|
||||
std::unique_ptr<Config> config,
|
||||
|
||||
@@ -27,9 +27,14 @@
|
||||
#include <xrpl/beast/utility/PropertyStream.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -279,6 +284,27 @@ make_Application(
|
||||
std::unique_ptr<Logs> logs,
|
||||
std::unique_ptr<TimeKeeper> timeKeeper);
|
||||
|
||||
/** Location of the receipt left behind when the server stops because it does
|
||||
not support a network amendment. */
|
||||
boost::filesystem::path
|
||||
amendmentBlockedFilePath(Config const& config);
|
||||
|
||||
/** Write the amendment-blocked receipt: a record for the operator of when the
|
||||
server stopped and which amendments it could not support. `amendments`
|
||||
holds one already-rendered line per unsupported amendment, and may be
|
||||
empty. Best effort -- any error is returned rather than thrown, and the
|
||||
caller is expected to continue shutting down either way. */
|
||||
boost::system::error_code
|
||||
writeAmendmentBlockedFile(
|
||||
Config const& config,
|
||||
std::vector<std::string> const& amendments);
|
||||
|
||||
/** Remove any amendment-blocked receipt left behind by a previous run.
|
||||
Returns true if a receipt was present and has been removed; sets `ec` if
|
||||
removal was attempted and failed. */
|
||||
bool
|
||||
removeAmendmentBlockedFile(Config const& config, boost::system::error_code& ec);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -809,6 +809,31 @@ run(int argc, char** argv)
|
||||
// No arguments. Run server.
|
||||
if (!vm.count("parameters"))
|
||||
{
|
||||
// Clear any receipt left by a previous amendment-blocked shutdown. It
|
||||
// records why the server stopped; it is not a lock. If this build
|
||||
// still does not support the amendment it will stop again and write a
|
||||
// fresh one, so there is nothing for the operator to delete by hand
|
||||
// and no way for a stale receipt to keep a working build down.
|
||||
// Standalone does not write receipts, so it does not clear them
|
||||
// either, leaving the file readable for diagnosis.
|
||||
if (!config->standalone())
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
auto const blockedFile = amendmentBlockedFilePath(*config);
|
||||
if (removeAmendmentBlockedFile(*config, ec))
|
||||
{
|
||||
JLOG(logs->journal("Application").warn())
|
||||
<< "Removed amendment-blocked receipt " << blockedFile
|
||||
<< " left by a previous run.";
|
||||
}
|
||||
else if (ec)
|
||||
{
|
||||
JLOG(logs->journal("Application").warn())
|
||||
<< "Could not remove amendment-blocked receipt "
|
||||
<< blockedFile << ": " << ec.message();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this comment can be removed in a future release -
|
||||
// say 1.7 or higher
|
||||
if (config->had_trailing_comments())
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <xrpl/protocol/STValidation.h>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -50,6 +51,17 @@ public:
|
||||
VoteBehavior const vote;
|
||||
};
|
||||
|
||||
/** An amendment seen on the network that this server has no code
|
||||
support for. */
|
||||
struct UnsupportedAmendment
|
||||
{
|
||||
uint256 id;
|
||||
|
||||
/** The time the amendment is expected to activate. Unset if it is
|
||||
already active. */
|
||||
std::optional<NetClock::time_point> expected;
|
||||
};
|
||||
|
||||
virtual ~AmendmentTable() = default;
|
||||
|
||||
virtual uint256
|
||||
@@ -80,6 +92,12 @@ public:
|
||||
virtual std::optional<NetClock::time_point>
|
||||
firstUnsupportedExpected() const = 0;
|
||||
|
||||
/** Amendments this server does not support that are already enabled, or
|
||||
that have reached majority and are expected to activate. Ordered by
|
||||
amendment id. Intended for operator-facing diagnostics. */
|
||||
virtual std::vector<UnsupportedAmendment>
|
||||
unsupportedAmendments() const = 0;
|
||||
|
||||
virtual Json::Value
|
||||
getJson(bool isAdmin) const = 0;
|
||||
|
||||
|
||||
@@ -25,12 +25,18 @@
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ReadView;
|
||||
|
||||
/*
|
||||
Validator key manifests
|
||||
-----------------------
|
||||
@@ -172,6 +178,16 @@ deserializeManifest(
|
||||
return deserializeManifest(makeSlice(s), journal);
|
||||
}
|
||||
|
||||
inline std::optional<Manifest>
|
||||
deserializeManifest(
|
||||
STObject const& st,
|
||||
beast::Journal journal = beast::Journal(beast::Journal::getNullSink()))
|
||||
{
|
||||
Serializer s;
|
||||
st.add(s);
|
||||
return deserializeManifest(makeSlice(s.peekData()), journal);
|
||||
}
|
||||
|
||||
template <
|
||||
class T,
|
||||
class = std::enable_if_t<
|
||||
@@ -264,9 +280,60 @@ private:
|
||||
/** Master public keys stored by current ephemeral public key. */
|
||||
hash_map<PublicKey, PublicKey> signingToMasterKeys_;
|
||||
|
||||
/** Master keys always offered to a peer, whatever their recency.
|
||||
|
||||
Set by pin(); in practice the master keys on the configured validator
|
||||
lists, which are the manifests consensus actually depends on.
|
||||
*/
|
||||
hash_set<PublicKey> pinned_;
|
||||
|
||||
/** Recency of use, keyed by master public key.
|
||||
|
||||
The structure of this map is guarded by mutex_ exactly as map_ is:
|
||||
entries are created next to it in applyManifest() and are never
|
||||
removed. The counters themselves are atomic, so recording a hit is a
|
||||
write to an atomic rather than a structural modification and is legal
|
||||
while only a shared lock is held.
|
||||
|
||||
This is deliberately not a second mutex. A second mutex would have to
|
||||
be ordered against mutex_, and that ordering would be an unenforced
|
||||
invariant that any future caller could invert.
|
||||
*/
|
||||
hash_map<PublicKey, std::atomic<std::uint64_t>> mutable lastUsed_;
|
||||
std::atomic<std::uint64_t> mutable tick_{0};
|
||||
|
||||
/** Ephemeral keys already probed against the ledger, and where.
|
||||
|
||||
Bounds the reads driven by incoming validations to one per key per
|
||||
ledger. A key that resolves does not come back here: the mapping then
|
||||
lives in signingToMasterKeys_ and applyLedgerSigningKey() answers from
|
||||
it before reaching this map.
|
||||
|
||||
Unlike lastUsed_ this is written structurally, so it is guarded by
|
||||
mutex_ in exclusive mode.
|
||||
*/
|
||||
hash_map<PublicKey, std::uint32_t> probed_;
|
||||
|
||||
/** Record that a manifest was looked up.
|
||||
|
||||
@pre The caller holds mutex_, shared or exclusive.
|
||||
*/
|
||||
void
|
||||
touch(PublicKey const& masterKey) const;
|
||||
|
||||
std::atomic<std::uint32_t> seq_{0};
|
||||
|
||||
public:
|
||||
/** Ceiling on the unpinned manifests offered to a newly connected peer. */
|
||||
static constexpr std::size_t gossipLimit = 64;
|
||||
|
||||
/** Ceiling on remembered ephemeral key probes before they are dropped.
|
||||
|
||||
A cache of negatives, so dropping it costs at most one extra ledger
|
||||
read per key.
|
||||
*/
|
||||
static constexpr std::size_t probeLimit = 4096;
|
||||
|
||||
explicit ManifestCache(
|
||||
beast::Journal j = beast::Journal(beast::Journal::getNullSink()))
|
||||
: j_(j)
|
||||
@@ -355,6 +422,90 @@ public:
|
||||
ManifestDisposition
|
||||
applyManifest(Manifest m);
|
||||
|
||||
/** Set the master keys that are always offered to peers.
|
||||
|
||||
Replaces any previous set. Bumps sequence() when the set actually
|
||||
changes, so a cached gossip message built from it is rebuilt.
|
||||
|
||||
@param keys Master public keys to pin
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
void
|
||||
pin(hash_set<PublicKey> keys);
|
||||
|
||||
/** Returns the sequence and serialized form of a held manifest.
|
||||
|
||||
Unlike getManifest() and getSequence(), a revoked master key is
|
||||
reported rather than skipped. Those two answer "what should I trust",
|
||||
for which a revocation is correctly nothing; a caller republishing what
|
||||
it holds needs the revocation most of all.
|
||||
|
||||
@param pk Master public key
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::optional<std::pair<std::uint32_t, std::string>>
|
||||
getRawManifest(PublicKey const& pk) const;
|
||||
|
||||
/** Ingest manifests published on-ledger.
|
||||
|
||||
Reads keylet::manifest() for each supplied master key, reconstructs any
|
||||
manifest found and feeds it through applyManifest(), so an on-chain
|
||||
manifest is subject to exactly the same staleness, revocation and
|
||||
key-reuse rules -- and the same signature check -- as one arriving by
|
||||
peer gossip or in a published list. It is a third source of manifests,
|
||||
not a more trusted one.
|
||||
|
||||
Probes a known key set rather than scanning the ledger's transactions:
|
||||
this costs one SHAMap read per key, and picks up manifests published in
|
||||
ledgers this node never saw.
|
||||
|
||||
@param view Ledger to read from
|
||||
@param masterKeys Master public keys to probe for
|
||||
|
||||
@return the number of manifests newly accepted
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::size_t
|
||||
applyLedger(ReadView const& view, hash_set<PublicKey> const& masterKeys);
|
||||
|
||||
/** Resolve an ephemeral signing key against a manifest published on-ledger.
|
||||
|
||||
applyLedger() probes a known master key set, which cannot help a key
|
||||
this node has no manifest for: the master key is exactly what is
|
||||
missing. SetManifest writes a second copy of every manifest keyed by
|
||||
its ephemeral key, so that case is one read rather than a search.
|
||||
|
||||
Anything found is fed through applyManifest(), so an on-chain manifest
|
||||
faces the same signature check and the same staleness, revocation and
|
||||
key-reuse rules as one arriving by gossip. The ledger is a transport
|
||||
here, not an authority: the answer is read back out of the cache rather
|
||||
than taken from the ledger object, because applyManifest() may decline
|
||||
it.
|
||||
|
||||
A key is probed at most once per ledger, and a key that resolves is
|
||||
answered from the cache thereafter without any ledger read.
|
||||
|
||||
@param view Ledger to read from
|
||||
@param signingKey Ephemeral public key to resolve
|
||||
|
||||
@return the master key now associated with signingKey, if any
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::optional<PublicKey>
|
||||
applyLedgerSigningKey(ReadView const& view, PublicKey const& signingKey);
|
||||
|
||||
/** Populate manifest cache with manifests in database and config.
|
||||
|
||||
@param dbCon Database connection with dbTable
|
||||
@@ -460,6 +611,70 @@ public:
|
||||
f(manifest);
|
||||
}
|
||||
}
|
||||
|
||||
/** Invokes the callback for the manifests worth offering a new peer.
|
||||
|
||||
Offering the whole cache means offering everything the node has ever
|
||||
seen, which grows without bound and is mostly of no use to the peer.
|
||||
This offers the pinned set plus up to gossipLimit further manifests,
|
||||
most recently used first. A manifest left out still reaches the peer by
|
||||
ordinary relay if it turns out to be needed.
|
||||
|
||||
@note Do not call ManifestCache member functions from within the
|
||||
callback. This can re-lock the mutex from the same thread, which is UB.
|
||||
@note Do not write ManifestCache member variables from
|
||||
within the callback. This can lead to data races.
|
||||
|
||||
@param pf Pre-function called with the maximum number of times f will be
|
||||
called (useful for memory allocations)
|
||||
|
||||
@param f Function called for each manifest
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
template <class PreFun, class EachFun>
|
||||
void
|
||||
for_each_gossip_manifest(PreFun&& pf, EachFun&& f) const
|
||||
{
|
||||
std::shared_lock lock{mutex_};
|
||||
|
||||
// Rank the unpinned entries by recency and keep the head. Pointers
|
||||
// into map_ stay valid: it is node based and the shared lock is held
|
||||
// throughout.
|
||||
std::vector<std::pair<std::uint64_t, PublicKey const*>> ranked;
|
||||
ranked.reserve(map_.size());
|
||||
for (auto const& [key, manifest] : map_)
|
||||
{
|
||||
(void)manifest;
|
||||
if (pinned_.count(key))
|
||||
continue;
|
||||
auto const used = lastUsed_.find(key);
|
||||
ranked.emplace_back(
|
||||
used == lastUsed_.end()
|
||||
? 0
|
||||
: used->second.load(std::memory_order_relaxed),
|
||||
&key);
|
||||
}
|
||||
|
||||
auto const keep = std::min(gossipLimit, ranked.size());
|
||||
std::partial_sort(
|
||||
ranked.begin(),
|
||||
ranked.begin() + keep,
|
||||
ranked.end(),
|
||||
[](auto const& a, auto const& b) { return a.first > b.first; });
|
||||
|
||||
// An upper bound: a pinned key need not have a manifest yet.
|
||||
pf(pinned_.size() + keep);
|
||||
|
||||
for (auto const& key : pinned_)
|
||||
if (auto const iter = map_.find(key); iter != map_.end())
|
||||
f(iter->second);
|
||||
|
||||
for (std::size_t i = 0; i < keep; ++i)
|
||||
f(map_.find(*ranked[i].second)->second);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -28,11 +28,13 @@
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/ledger/OrderBookDB.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/main/LoadManager.h>
|
||||
#include <xrpld/app/misc/AmendmentTable.h>
|
||||
#include <xrpld/app/misc/DeliverMax.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/LoadFeeTrack.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/misc/StateAccounting.h>
|
||||
#include <xrpld/app/misc/Transaction.h>
|
||||
@@ -42,6 +44,7 @@
|
||||
#include <xrpld/app/misc/detail/AccountTxPaging.h>
|
||||
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/detail/SetManifest.h> // makeSetManifestTx
|
||||
#include <xrpld/consensus/Consensus.h>
|
||||
#include <xrpld/consensus/ConsensusParms.h>
|
||||
#include <xrpld/overlay/Cluster.h>
|
||||
@@ -54,6 +57,7 @@
|
||||
#include <xrpld/rpc/MPTokenIssuanceID.h>
|
||||
#include <xrpld/rpc/ServerHandler.h>
|
||||
#include <xrpld/rpc/detail/UDPInfoSub.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/UptimeClock.h>
|
||||
#include <xrpl/basics/mulDiv.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
@@ -68,6 +72,7 @@
|
||||
#include <xrpl/protocol/MultiApiJson.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <xrpl/resource/ResourceManager.h>
|
||||
@@ -311,6 +316,8 @@ private:
|
||||
switchLastClosedLedger(std::shared_ptr<Ledger const> const& newLCL);
|
||||
bool
|
||||
checkLastClosedLedger(const Overlay::PeerSequence&, uint256& networkClosed);
|
||||
void
|
||||
publishNewerManifests(ReadView const& ledger);
|
||||
|
||||
public:
|
||||
bool
|
||||
@@ -1156,6 +1163,72 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::publishNewerManifests(ReadView const& ledger)
|
||||
{
|
||||
for (auto const& pk : app_.validators().getTrustedMasterKeys())
|
||||
{
|
||||
// Only for validators that have opted in by publishing on-ledger
|
||||
// already. Submitting spends the master key account's balance, so an
|
||||
// account that has never used the feature is left alone.
|
||||
auto const sleMan = ledger.read(keylet::manifest(pk));
|
||||
if (!sleMan)
|
||||
continue;
|
||||
|
||||
auto const held = app_.validatorManifests().getRawManifest(pk);
|
||||
if (!held || held->first <= sleMan->getFieldU32(sfSequence))
|
||||
continue;
|
||||
|
||||
auto const hex = makeSetManifestTx(
|
||||
makeSlice(held->second),
|
||||
app_.config().NETWORK_ID,
|
||||
*app_.openLedger().current(),
|
||||
app_.journal("Manifest"));
|
||||
|
||||
auto const blob = hex ? strUnHex(*hex) : std::nullopt;
|
||||
if (!blob || blob->empty())
|
||||
continue;
|
||||
|
||||
std::shared_ptr<STTx const> stTx;
|
||||
std::string reason;
|
||||
std::shared_ptr<Transaction> tx;
|
||||
try
|
||||
{
|
||||
SerialIter sit{makeSlice(*blob)};
|
||||
stTx = std::make_shared<STTx const>(std::ref(sit));
|
||||
tx = std::make_shared<Transaction>(stTx, reason, app_);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(m_journal.warn())
|
||||
<< "publishNewerManifests: " << e.what() << " " << reason;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tx->getStatus() != NEW)
|
||||
continue;
|
||||
|
||||
// The master key account pays. Skip rather than submit something that
|
||||
// can only fail: this runs every ledger, so an unfunded validator
|
||||
// would otherwise be retried forever.
|
||||
auto const sleAcct = ledger.read(keylet::account(calcAccountID(pk)));
|
||||
if (!sleAcct ||
|
||||
sleAcct->getFieldAmount(sfBalance).xrp() < (*stTx)[sfFee].xrp())
|
||||
continue;
|
||||
|
||||
JLOG(m_journal.info())
|
||||
<< "publishNewerManifests: publishing manifest seq " << held->first
|
||||
<< " for " << toBase58(TokenType::NodePublic, pk);
|
||||
|
||||
// Submitted from the job queue because this runs on the consensus
|
||||
// thread, which must not block on transaction processing.
|
||||
m_job_queue.addJob(jtTRANSACTION, "publishManifest", [this, tx]() {
|
||||
auto t = tx;
|
||||
processTransaction(t, false, false, FailHard::no);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::processTransaction(
|
||||
std::shared_ptr<Transaction>& transaction,
|
||||
@@ -1629,11 +1702,68 @@ NetworkOPsImp::isAmendmentBlocked()
|
||||
return amendmentBlocked_;
|
||||
}
|
||||
|
||||
// Render the unsupported amendments for the operator-facing receipt. This is
|
||||
// the only identification available: an amendment this build does not support
|
||||
// has no name here, so the id is what the operator matches against the
|
||||
// release notes.
|
||||
static std::vector<std::string>
|
||||
describeUnsupportedAmendments(AmendmentTable const& table)
|
||||
{
|
||||
std::vector<std::string> lines;
|
||||
|
||||
for (auto const& amendment : table.unsupportedAmendments())
|
||||
{
|
||||
std::ostringstream ss;
|
||||
ss << to_string(amendment.id);
|
||||
if (amendment.expected)
|
||||
ss << " (expected to activate "
|
||||
<< to_string_iso(*amendment.expected) << ")";
|
||||
else
|
||||
ss << " (already active)";
|
||||
lines.push_back(ss.str());
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::setAmendmentBlocked()
|
||||
{
|
||||
amendmentBlocked_ = true;
|
||||
// Idempotent: this is reached from Change::applyAmendment (i.e. from the
|
||||
// transaction application path, which is not guarded by isBlocked()) as
|
||||
// well as from LedgerMaster::setValidLedger. Writing the receipt and
|
||||
// logging once per process is enough, and it keeps the synchronous file
|
||||
// write out of any subsequent ledger apply.
|
||||
if (amendmentBlocked_.exchange(true))
|
||||
return;
|
||||
|
||||
setMode(OperatingMode::CONNECTED);
|
||||
if (!app_.config().standalone())
|
||||
{
|
||||
auto const blockedFile = amendmentBlockedFilePath(app_.config());
|
||||
if (auto const ec = writeAmendmentBlockedFile(
|
||||
app_.config(),
|
||||
describeUnsupportedAmendments(app_.getAmendmentTable())))
|
||||
{
|
||||
JLOG(m_journal.fatal())
|
||||
<< "Could not write amendment-blocked receipt " << blockedFile
|
||||
<< ": " << ec.message();
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(m_journal.fatal())
|
||||
<< "Amendment-blocked receipt written to " << blockedFile;
|
||||
}
|
||||
JLOG(m_journal.fatal())
|
||||
<< "This version of xahaud does not support a network amendment. "
|
||||
"The amendment will activate soon or is already active. "
|
||||
"The server will stop. Upgrade xahaud before you restart the "
|
||||
"server.";
|
||||
app_.signalStop(
|
||||
"Unsupported network amendment. Upgrade xahaud before you restart "
|
||||
"the server. Details: " +
|
||||
blockedFile.string());
|
||||
}
|
||||
}
|
||||
|
||||
inline bool
|
||||
@@ -1779,6 +1909,27 @@ NetworkOPsImp::checkLastClosedLedger(
|
||||
return true;
|
||||
}
|
||||
|
||||
// True if `view` enables an amendment this binary does not implement. Reads
|
||||
// only the amendments ledger entry, so it is safe to call once transaction
|
||||
// deserialization is already known to be failing.
|
||||
static bool
|
||||
ledgerHasUnsupportedAmendments(
|
||||
AmendmentTable const& table,
|
||||
ReadView const& view)
|
||||
{
|
||||
auto const sle = view.read(keylet::amendments());
|
||||
if (!sle || !sle->isFieldPresent(sfAmendments))
|
||||
return false;
|
||||
|
||||
for (auto const& amendment : sle->getFieldV256(sfAmendments))
|
||||
{
|
||||
if (!table.isSupported(amendment))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::switchLastClosedLedger(
|
||||
std::shared_ptr<Ledger const> const& newLCL)
|
||||
@@ -1789,8 +1940,32 @@ NetworkOPsImp::switchLastClosedLedger(
|
||||
|
||||
clearNeedNetworkLedger();
|
||||
|
||||
// Update fee computations.
|
||||
app_.getTxQ().processClosedLedger(app_, *newLCL, true);
|
||||
// Update fee computations. May throw if the ledger contains
|
||||
// transactions with fields unknown to this binary (e.g. after an
|
||||
// unsupported amendment activates). Catch to allow graceful shutdown.
|
||||
try
|
||||
{
|
||||
app_.getTxQ().processClosedLedger(app_, *newLCL, true);
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
// Do not decide this on amendmentBlocked_ alone. A JUMP can happen
|
||||
// before any validated ledger has been processed -- e.g. immediately
|
||||
// after a restart, which is precisely the case that crashed -- so the
|
||||
// flag may still be clear here. Ask the ledger itself instead.
|
||||
// Anything else is a real bug and must propagate.
|
||||
if (!amendmentBlocked_ &&
|
||||
!ledgerHasUnsupportedAmendments(app_.getAmendmentTable(), *newLCL))
|
||||
throw;
|
||||
|
||||
JLOG(m_journal.error()) << "Failed to process closed ledger "
|
||||
<< newLCL->info().seq << ": " << e.what();
|
||||
|
||||
// No-op if we are already blocked; otherwise this starts the
|
||||
// shutdown that should have been started before activation.
|
||||
setAmendmentBlocked();
|
||||
return;
|
||||
}
|
||||
|
||||
// Caller must own master lock
|
||||
{
|
||||
@@ -1874,6 +2049,23 @@ NetworkOPsImp::beginConsensus(
|
||||
|
||||
if (prevLedger->rules().enabled(featureNegativeUNL))
|
||||
app_.validators().setNegativeUNL(prevLedger->negativeUNL());
|
||||
// Pull in any manifests published on-ledger before the trusted set is
|
||||
// recomputed, so a validator that rotated its ephemeral key on-chain is
|
||||
// resolved to the new signing key in this same round. The master keys come
|
||||
// from the published lists, so this needs no bootstrap: only the ephemeral
|
||||
// half of the mapping ever comes from a manifest.
|
||||
if (prevLedger->rules().enabled(featureOnChainManifests))
|
||||
{
|
||||
app_.validatorManifests().applyLedger(
|
||||
*prevLedger, app_.validators().getTrustedMasterKeys());
|
||||
|
||||
// The reverse of applyLedger above. Manifests reach us by peer gossip
|
||||
// and in published validator lists, both of which can arrive before
|
||||
// the validator gets around to publishing on-chain, so the cache may
|
||||
// hold something newer than the ledger does.
|
||||
publishNewerManifests(*prevLedger);
|
||||
}
|
||||
|
||||
TrustChanges const changes = app_.validators().updateTrusted(
|
||||
app_.getValidations().getCurrentNodeIDs(),
|
||||
closingInfo.parentCloseTime,
|
||||
@@ -1881,6 +2073,10 @@ NetworkOPsImp::beginConsensus(
|
||||
app_.overlay(),
|
||||
app_.getHashRouter());
|
||||
|
||||
// Pin the trusted master keys so they are always offered to a new peer and
|
||||
// cannot be crowded out of the gossip set by more recently used manifests.
|
||||
app_.validatorManifests().pin(app_.validators().getTrustedMasterKeys());
|
||||
|
||||
if (!changes.added.empty() || !changes.removed.empty())
|
||||
{
|
||||
app_.getValidations().trustChanged(changes.added, changes.removed);
|
||||
@@ -2449,7 +2645,7 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
|
||||
"may be incorrectly configured or some [validator_list_sites] "
|
||||
"may be unreachable.";
|
||||
}
|
||||
if (admin && isAmendmentWarned())
|
||||
if (isAmendmentWarned())
|
||||
{
|
||||
Json::Value& w = warnings.append(Json::objectValue);
|
||||
w[jss::id] = warnRPC_UNSUPPORTED_MAJORITY;
|
||||
|
||||
@@ -432,6 +432,11 @@ private:
|
||||
// will be enabled.
|
||||
std::optional<NetClock::time_point> firstUnsupportedExpected_;
|
||||
|
||||
// Unsupported amendments that have reached majority, and the time each
|
||||
// is expected to activate. Recomputed alongside
|
||||
// firstUnsupportedExpected_, so it clears when majority is lost.
|
||||
std::vector<std::pair<uint256, NetClock::time_point>> unsupportedMajority_;
|
||||
|
||||
beast::Journal const j_;
|
||||
|
||||
// Database which persists veto/unveto vote
|
||||
@@ -495,6 +500,9 @@ public:
|
||||
std::optional<NetClock::time_point>
|
||||
firstUnsupportedExpected() const override;
|
||||
|
||||
std::vector<UnsupportedAmendment>
|
||||
unsupportedAmendments() const override;
|
||||
|
||||
Json::Value
|
||||
getJson(bool isAdmin) const override;
|
||||
Json::Value
|
||||
@@ -807,6 +815,36 @@ AmendmentTableImpl::firstUnsupportedExpected() const
|
||||
return firstUnsupportedExpected_;
|
||||
}
|
||||
|
||||
std::vector<AmendmentTable::UnsupportedAmendment>
|
||||
AmendmentTableImpl::unsupportedAmendments() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
std::vector<UnsupportedAmendment> result;
|
||||
|
||||
// Already active.
|
||||
for (auto const& [id, state] : amendmentMap_)
|
||||
{
|
||||
if (state.enabled && !state.supported)
|
||||
result.push_back({id, std::nullopt});
|
||||
}
|
||||
|
||||
// Reached majority, not yet active.
|
||||
for (auto const& entry : unsupportedMajority_)
|
||||
{
|
||||
if (std::none_of(result.begin(), result.end(), [&entry](auto const& u) {
|
||||
return u.id == entry.first;
|
||||
}))
|
||||
result.push_back({entry.first, entry.second});
|
||||
}
|
||||
|
||||
std::sort(result.begin(), result.end(), [](auto const& a, auto const& b) {
|
||||
return a.id < b.id;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uint256>
|
||||
AmendmentTableImpl::doValidation(std::set<uint256> const& enabled) const
|
||||
{
|
||||
@@ -967,6 +1005,7 @@ AmendmentTableImpl::doValidatedLedger(
|
||||
// if it's currently set. If it's not set when the loop is done, then any
|
||||
// prior unknown amendments have lost majority.
|
||||
firstUnsupportedExpected_.reset();
|
||||
unsupportedMajority_.clear();
|
||||
for (auto const& [hash, time] : majority)
|
||||
{
|
||||
AmendmentState& s = add(hash, lock);
|
||||
@@ -978,6 +1017,7 @@ AmendmentTableImpl::doValidatedLedger(
|
||||
{
|
||||
JLOG(j_.info()) << "Unsupported amendment " << hash
|
||||
<< " reached majority at " << to_string(time);
|
||||
unsupportedMajority_.emplace_back(hash, time + majorityTime_);
|
||||
if (!firstUnsupportedExpected_ || firstUnsupportedExpected_ > time)
|
||||
firstUnsupportedExpected_ = time;
|
||||
}
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/rdb/Wallet.h>
|
||||
#include <xrpld/core/DatabaseCon.h>
|
||||
#include <xrpld/ledger/ReadView.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
|
||||
@@ -54,7 +56,6 @@ deserializeManifest(Slice s, beast::Journal journal)
|
||||
{
|
||||
if (s.empty())
|
||||
return std::nullopt;
|
||||
|
||||
static SOTemplate const manifestFormat{
|
||||
// A manifest must include:
|
||||
// - the master public key
|
||||
@@ -301,7 +302,10 @@ ManifestCache::getSigningKey(PublicKey const& pk) const
|
||||
auto const iter = map_.find(pk);
|
||||
|
||||
if (iter != map_.end() && !iter->second.revoked())
|
||||
{
|
||||
touch(pk);
|
||||
return iter->second.signingKey;
|
||||
}
|
||||
|
||||
return pk;
|
||||
}
|
||||
@@ -313,7 +317,10 @@ ManifestCache::getMasterKey(PublicKey const& pk) const
|
||||
|
||||
if (auto const iter = signingToMasterKeys_.find(pk);
|
||||
iter != signingToMasterKeys_.end())
|
||||
{
|
||||
touch(iter->second);
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
return pk;
|
||||
}
|
||||
@@ -349,7 +356,10 @@ ManifestCache::getManifest(PublicKey const& pk) const
|
||||
auto const iter = map_.find(pk);
|
||||
|
||||
if (iter != map_.end() && !iter->second.revoked())
|
||||
{
|
||||
touch(pk);
|
||||
return iter->second.serialized;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -361,11 +371,169 @@ ManifestCache::revoked(PublicKey const& pk) const
|
||||
auto const iter = map_.find(pk);
|
||||
|
||||
if (iter != map_.end())
|
||||
{
|
||||
touch(pk);
|
||||
return iter->second.revoked();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<std::pair<std::uint32_t, std::string>>
|
||||
ManifestCache::getRawManifest(PublicKey const& pk) const
|
||||
{
|
||||
std::shared_lock lock{mutex_};
|
||||
|
||||
if (auto const iter = map_.find(pk); iter != map_.end())
|
||||
{
|
||||
touch(pk);
|
||||
return std::make_pair(iter->second.sequence, iter->second.serialized);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
ManifestCache::touch(PublicKey const& masterKey) const
|
||||
{
|
||||
// find() rather than operator[]: inserting here would be a structural
|
||||
// modification, and callers hold mutex_ only in shared mode. The entry is
|
||||
// created in applyManifest() alongside the manifest itself, so a lookup
|
||||
// that hit map_ always finds one here too.
|
||||
if (auto const iter = lastUsed_.find(masterKey); iter != lastUsed_.end())
|
||||
iter->second.store(++tick_, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void
|
||||
ManifestCache::pin(hash_set<PublicKey> keys)
|
||||
{
|
||||
std::lock_guard lock{mutex_};
|
||||
|
||||
if (keys == pinned_)
|
||||
return;
|
||||
|
||||
pinned_ = std::move(keys);
|
||||
|
||||
// The pinned set is part of what a gossip message contains, so a change to
|
||||
// it has to invalidate any message cached against this sequence.
|
||||
++seq_;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** Rebuild the manifest a ltMANIFEST object was written from.
|
||||
|
||||
The object is a lossless mirror written by SetManifest::doApply, so this
|
||||
round-trip is byte-identical to the blob the master key signed and verify()
|
||||
succeeds, or the manifest is discarded. Presence matters: sfVersion is
|
||||
soeDEFAULT in the manifest format and must not be materialised.
|
||||
*/
|
||||
std::optional<Manifest>
|
||||
manifestFromSLE(SLE const& sle, beast::Journal j)
|
||||
{
|
||||
STObject st{sfGeneric};
|
||||
st.setFieldU32(sfSequence, sle.getFieldU32(sfSequence));
|
||||
st.setFieldVL(sfPublicKey, sle.getFieldVL(sfPublicKey));
|
||||
st.setFieldVL(sfMasterSignature, sle.getFieldVL(sfMasterSignature));
|
||||
for (auto const& sf :
|
||||
{std::cref(sfSigningPubKey),
|
||||
std::cref(sfSignature),
|
||||
std::cref(sfDomain)})
|
||||
if (sle.isFieldPresent(sf.get()))
|
||||
st.setFieldVL(sf.get(), sle.getFieldVL(sf.get()));
|
||||
if (sle.isFieldPresent(sfVersion))
|
||||
st.setFieldU16(sfVersion, sle.getFieldU16(sfVersion));
|
||||
|
||||
return deserializeManifest(st, j);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::size_t
|
||||
ManifestCache::applyLedger(
|
||||
ReadView const& view,
|
||||
hash_set<PublicKey> const& masterKeys)
|
||||
{
|
||||
std::size_t accepted = 0;
|
||||
|
||||
for (auto const& pk : masterKeys)
|
||||
{
|
||||
auto const sle = view.read(keylet::manifest(pk));
|
||||
if (!sle)
|
||||
continue;
|
||||
|
||||
// Cheap reject before rebuilding: applyManifest() would call this
|
||||
// stale anyway, and the signature check is the expensive part.
|
||||
if (auto const seq = getSequence(pk);
|
||||
seq && *seq >= sle->getFieldU32(sfSequence))
|
||||
continue;
|
||||
|
||||
if (auto mo = manifestFromSLE(*sle, j_); mo &&
|
||||
applyManifest(std::move(*mo)) == ManifestDisposition::accepted)
|
||||
++accepted;
|
||||
}
|
||||
|
||||
return accepted;
|
||||
}
|
||||
|
||||
std::optional<PublicKey>
|
||||
ManifestCache::applyLedgerSigningKey(
|
||||
ReadView const& view,
|
||||
PublicKey const& signingKey)
|
||||
{
|
||||
auto const held = [this, &signingKey]() -> std::optional<PublicKey> {
|
||||
std::shared_lock lock{mutex_};
|
||||
|
||||
if (auto const iter = signingToMasterKeys_.find(signingKey);
|
||||
iter != signingToMasterKeys_.end())
|
||||
{
|
||||
touch(iter->second);
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
// A manifest for this key may have arrived by gossip or in a published
|
||||
// list while the caller was deciding to ask.
|
||||
if (auto const known = held())
|
||||
return known;
|
||||
|
||||
{
|
||||
std::lock_guard lock{mutex_};
|
||||
|
||||
if (probed_.size() >= probeLimit)
|
||||
probed_.clear();
|
||||
|
||||
// One read per key per ledger. Reaching here already cost the sender a
|
||||
// signature and this node a verification, so the read is not the
|
||||
// cheapest thing an unknown key can ask for; the cap is to stop
|
||||
// repeating it, not to stop anyone.
|
||||
auto const seq = view.info().seq;
|
||||
auto const [iter, inserted] = probed_.try_emplace(signingKey, seq);
|
||||
if (!inserted && iter->second == seq)
|
||||
return std::nullopt;
|
||||
|
||||
iter->second = seq;
|
||||
}
|
||||
|
||||
auto const sle = view.read(keylet::manifest(signingKey));
|
||||
if (!sle)
|
||||
return std::nullopt;
|
||||
|
||||
// Every manifest is written at both its master and its ephemeral keylet,
|
||||
// so an object here is either the manifest naming signingKey as its
|
||||
// ephemeral key -- the case worth having -- or the manifest of a master
|
||||
// key that is what was asked about. Ingesting either is correct, and
|
||||
// applyManifest() verifies both signatures, so nothing found here can
|
||||
// assert a binding its key holder did not sign for.
|
||||
if (auto mo = manifestFromSLE(*sle, j_))
|
||||
applyManifest(std::move(*mo));
|
||||
|
||||
// Only a signing key resolves: a master key is its own master.
|
||||
return held();
|
||||
}
|
||||
|
||||
ManifestDisposition
|
||||
ManifestCache::applyManifest(Manifest m)
|
||||
{
|
||||
@@ -499,6 +667,10 @@ ManifestCache::applyManifest(Manifest m)
|
||||
if (!revoked)
|
||||
signingToMasterKeys_.emplace(*m.signingKey, m.masterKey);
|
||||
|
||||
// Kept in step with map_ so touch() never has to insert; see
|
||||
// lastUsed_.
|
||||
lastUsed_.try_emplace(m.masterKey, 0);
|
||||
|
||||
auto masterKey = m.masterKey;
|
||||
map_.emplace(std::move(masterKey), std::move(m));
|
||||
|
||||
|
||||
@@ -1953,13 +1953,23 @@ TxQ::tryDirectApply(
|
||||
const bool isFirstImport = !sleAccount &&
|
||||
view.rules().enabled(featureImport) && tx->getTxnType() == ttIMPORT;
|
||||
|
||||
// A manifest txn is pinned to sfSequence 0 (Transactor::checkSeqProxy), so
|
||||
// it can never match the account sequence. Direct-apply it like a first
|
||||
// Import: letting it fall through to the queue would reject it outright on
|
||||
// sequence rather than hold it. Manifests are therefore exempt from fee
|
||||
// escalation, since requiredFeeLevel is not consulted for them.
|
||||
const bool isManifest = view.rules().enabled(featureOnChainManifests) &&
|
||||
tx->getTxnType() == ttMANIFEST_SET;
|
||||
|
||||
const bool bypassQueue = isFirstImport || isManifest;
|
||||
|
||||
// Don't attempt to direct apply if the account is not in the ledger.
|
||||
if (!sleAccount && !isFirstImport)
|
||||
return {};
|
||||
|
||||
std::optional<SeqProxy> txSeqProx;
|
||||
|
||||
if (!isFirstImport)
|
||||
if (!bypassQueue)
|
||||
{
|
||||
SeqProxy const acctSeqProx =
|
||||
SeqProxy::sequence((*sleAccount)[sfSequence]);
|
||||
@@ -1972,7 +1982,7 @@ TxQ::tryDirectApply(
|
||||
}
|
||||
|
||||
FeeLevel64 const requiredFeeLevel =
|
||||
isFirstImport ? FeeLevel64{0} : [this, &view, flags]() {
|
||||
bypassQueue ? FeeLevel64{0} : [this, &view, flags]() {
|
||||
std::lock_guard lock(mutex_);
|
||||
return getRequiredFeeLevel(
|
||||
view, flags, feeMetrics_.getSnapshot(), lock);
|
||||
|
||||
@@ -2033,9 +2033,11 @@ ValidatorList::updateTrusted(
|
||||
trustChanges.added.insert(calcNodeID(val.first));
|
||||
}
|
||||
|
||||
// If there were any changes, we need to update the ephemeral signing
|
||||
// keys:
|
||||
if (!trustChanges.added.empty() || !trustChanges.removed.empty())
|
||||
// Rebuilt on every call rather than only when membership changed: a
|
||||
// manifest that rotates a validator's ephemeral key changes no membership
|
||||
// at all, so a membership-gated rebuild would leave trustedSigningKeys_
|
||||
// stale and the rotated validator's validations would stop counting. This
|
||||
// is a handful of map lookups over the UNL.
|
||||
{
|
||||
trustedSigningKeys_.clear();
|
||||
|
||||
|
||||
@@ -612,6 +612,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltMPTOKEN:
|
||||
case ltCREDENTIAL:
|
||||
case ltPERMISSIONED_DOMAIN:
|
||||
case ltMANIFEST:
|
||||
break;
|
||||
default:
|
||||
invalidTypeAdded_ = true;
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/tx/detail/SetManifest.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/ledger/View.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h> // strUnHex
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h> // mulRatio
|
||||
#include <xrpl/protocol/serialize.h>
|
||||
#include <xrpl/protocol/st.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TxConsequences
|
||||
SetManifest::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
return TxConsequences{ctx.tx, TxConsequences::normal};
|
||||
}
|
||||
|
||||
NotTEC
|
||||
SetManifest::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureOnChainManifests))
|
||||
return temDISABLED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
auto& tx = ctx.tx;
|
||||
auto& j = ctx.j;
|
||||
|
||||
if (tx.getFlags() & tfUniversalMask)
|
||||
{
|
||||
JLOG(j.warn()) << "SetManifest: Invalid flags set.";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
// rules:
|
||||
// 1. sfManifest must match the manifest template and be validly signed
|
||||
// 2. the signingpubkey must match the master r-address
|
||||
// 3. manifest must not be already revoked
|
||||
|
||||
STObject const& obj =
|
||||
const_cast<ripple::STTx&>(tx).getField(sfManifest).downcast<STObject>();
|
||||
|
||||
// 1. sfManifest must match the manifest template and be validly signed
|
||||
auto manifest = deserializeManifest(obj, j);
|
||||
|
||||
if (!manifest.has_value())
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "SetManifest: invalid manifest passed (parseManifest failed).";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (!manifest->verify())
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "SetManifest: invalid manifest passed (manifest.verify failed).";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// 2. master must match r-address
|
||||
auto wantID = calcAccountID(manifest->masterKey);
|
||||
if (ctx.tx.getAccountID(sfAccount) != wantID)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "SetManifest: master key must match sfAccount (r-address).";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// 3. not already revoked will be checked in preclaim because it depends on
|
||||
// lgr state
|
||||
|
||||
// 4. the envelope carries no account signature: authority comes solely
|
||||
// from the manifest's own master/ephemeral signatures, which do not cover
|
||||
// the envelope. Pin every envelope field a relayer could otherwise choose.
|
||||
// The shape below must match the one checkValidity() recognises, or the
|
||||
// txn falls through to the ordinary signature path and is rejected there.
|
||||
// sfFee cannot be bounded here because the computed base fee is not in
|
||||
// scope until preclaim; checkFee() bounds it instead.
|
||||
if (!tx.isFieldPresent(sfSigningPubKey) || !tx.getSigningPubKey().empty() ||
|
||||
!tx.isFieldPresent(sfTxnSignature) || !tx.getSignature().empty() ||
|
||||
tx.isFieldPresent(sfSigners) || tx.isFieldPresent(sfAccountTxnID) ||
|
||||
tx.isFieldPresent(sfTicketSequence) || tx.getFieldU32(sfSequence) != 0)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "SetManifest: envelope must be unsigned with Sequence 0.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
SetManifest::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (!ctx.view.rules().enabled(featureOnChainManifests))
|
||||
return temDISABLED;
|
||||
|
||||
auto const id = ctx.tx[sfAccount];
|
||||
|
||||
// The account must exist: it pays the fee and anchors sfManifestID.
|
||||
auto const sle = ctx.view.read(keylet::account(id));
|
||||
if (!sle)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
STObject const& newObj = const_cast<ripple::STTx&>(ctx.tx)
|
||||
.getField(sfManifest)
|
||||
.downcast<STObject>();
|
||||
|
||||
auto const newManifest = deserializeManifest(newObj, ctx.j);
|
||||
if (!newManifest)
|
||||
return tefINTERNAL; // preflight already parsed this successfully
|
||||
|
||||
// Replay protection. A byte-identical resubmission is rejected as
|
||||
// tefALREADY by checkPriorTxAndLastLedger, but sfFee may vary within the
|
||||
// band checkFee() allows, so the same manifest can also arrive under a
|
||||
// different txid. The strictly-increasing sequence test below is what
|
||||
// covers that, both within this ledger and in every later one. Either
|
||||
// result is tef, so a replay is never included and never claims a fee.
|
||||
if (sle->isFieldPresent(sfManifestID))
|
||||
{
|
||||
// A dangling sfManifestID is a corrupt ledger; doApply reports it.
|
||||
if (auto const sleOld = ctx.view.read(
|
||||
Keylet{ltMANIFEST, sle->getFieldH256(sfManifestID)}))
|
||||
{
|
||||
if (sleOld->getFieldU32(sfSequence) ==
|
||||
std::numeric_limits<std::uint32_t>::max())
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "SetManifest: New manifest submitted for "
|
||||
"revoked master. "
|
||||
<< id;
|
||||
return tefREVOKED_MANIFEST;
|
||||
}
|
||||
|
||||
if (newManifest->sequence <= sleOld->getFieldU32(sfSequence))
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "SetManifest: Manifest sequence already passed. " << id;
|
||||
return tefPAST_MANIFEST_SEQ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On-chain equivalent of the badMasterKey/badEphemeralKey sanity checks in
|
||||
// ManifestCache::applyManifest. keylet::manifest(masterKey) is 1:1 with the
|
||||
// account, but keylet::manifest(signingKey) is not: without this, a
|
||||
// manifest naming another validator's key as its ephemeral key would
|
||||
// collide with -- and clobber -- that validator's object.
|
||||
if (newManifest->signingKey)
|
||||
{
|
||||
auto const sleEph =
|
||||
ctx.view.read(keylet::manifest(*newManifest->signingKey));
|
||||
if (sleEph && sleEph->getAccountID(sfAccount) != id)
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "SetManifest: Ephemeral key already claimed by another "
|
||||
"account. "
|
||||
<< id;
|
||||
return tecDUPLICATE;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SetManifest::doApply()
|
||||
{
|
||||
auto sle = view().peek(keylet::account(account_));
|
||||
if (!sle)
|
||||
return tefINTERNAL;
|
||||
|
||||
STObject const& obj = const_cast<ripple::STTx&>(ctx_.tx)
|
||||
.getField(sfManifest)
|
||||
.downcast<STObject>();
|
||||
|
||||
auto const manifest = deserializeManifest(obj, j_);
|
||||
|
||||
// Both of these were established in preflight.
|
||||
if (!manifest || calcAccountID(manifest->masterKey) != account_)
|
||||
return tefINTERNAL;
|
||||
|
||||
// A manifest is stored twice so it can be found from either key:
|
||||
// keylet::manifest(masterKey) -> obj1, sfManifestID -> obj2
|
||||
// keylet::manifest(signingKey) -> obj2, sfManifestID -> obj1
|
||||
// A revoked manifest has no signing key, so it exists only as obj1 with no
|
||||
// sfManifestID. Both copies are erased and rewritten on every update so
|
||||
// they can never drift apart.
|
||||
if (sle->isFieldPresent(sfManifestID))
|
||||
{
|
||||
uint256 const firstID = sle->getFieldH256(sfManifestID);
|
||||
auto const sleMan1 = view().peek(Keylet{ltMANIFEST, firstID});
|
||||
if (!sleMan1 || sleMan1->getAccountID(sfAccount) != account_)
|
||||
{
|
||||
JLOG(j_.error()) << "SetManifest: Old manifest object missing or "
|
||||
"misowned (ID1) !! "
|
||||
<< strHex(firstID);
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
// Absent when the previous manifest was a revocation.
|
||||
if (sleMan1->isFieldPresent(sfManifestID))
|
||||
{
|
||||
uint256 const secondID = sleMan1->getFieldH256(sfManifestID);
|
||||
auto const sleMan2 = view().peek(Keylet{ltMANIFEST, secondID});
|
||||
if (secondID == firstID || !sleMan2 ||
|
||||
sleMan2->getAccountID(sfAccount) != account_)
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "SetManifest: Old manifest object missing, misowned or "
|
||||
"self-referential (ID2) !! "
|
||||
<< strHex(secondID);
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
view().erase(sleMan2);
|
||||
}
|
||||
|
||||
view().erase(sleMan1);
|
||||
}
|
||||
|
||||
Keylet const klMan1 = keylet::manifest(manifest->masterKey);
|
||||
std::optional<Keylet> klMan2;
|
||||
if (!manifest->revoked() && manifest->signingKey)
|
||||
klMan2 = keylet::manifest(*manifest->signingKey);
|
||||
|
||||
// Neither key may still be occupied: preclaim rejects an ephemeral key held
|
||||
// by another account, and the block above cleared this account's own
|
||||
// copies.
|
||||
if (view().exists(klMan1) || (klMan2 && view().exists(*klMan2)))
|
||||
{
|
||||
JLOG(j_.error()) << "SetManifest: Manifest keylet already occupied !! "
|
||||
<< strHex(klMan1.key);
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
// Mirror the manifest losslessly, signatures included, so any node can
|
||||
// reconstruct and independently verify it (ManifestCache::applyLedger).
|
||||
// Field *presence* is copied faithfully: sfVersion is soeDEFAULT in the
|
||||
// manifest format, so materialising an absent one would alter the signed
|
||||
// payload and break verification.
|
||||
auto const write = [&](Keylet const& kl,
|
||||
std::optional<uint256> const& other) {
|
||||
auto sleMan = std::make_shared<SLE>(kl);
|
||||
sleMan->setAccountID(sfAccount, account_);
|
||||
sleMan->setFieldU32(sfSequence, obj.getFieldU32(sfSequence));
|
||||
sleMan->setFieldVL(sfPublicKey, obj.getFieldVL(sfPublicKey));
|
||||
sleMan->setFieldVL(
|
||||
sfMasterSignature, obj.getFieldVL(sfMasterSignature));
|
||||
if (obj.isFieldPresent(sfVersion))
|
||||
sleMan->setFieldU16(sfVersion, obj.getFieldU16(sfVersion));
|
||||
if (obj.isFieldPresent(sfSigningPubKey))
|
||||
sleMan->setFieldVL(
|
||||
sfSigningPubKey, obj.getFieldVL(sfSigningPubKey));
|
||||
if (obj.isFieldPresent(sfSignature))
|
||||
sleMan->setFieldVL(sfSignature, obj.getFieldVL(sfSignature));
|
||||
if (obj.isFieldPresent(sfDomain))
|
||||
sleMan->setFieldVL(sfDomain, obj.getFieldVL(sfDomain));
|
||||
if (other)
|
||||
sleMan->setFieldH256(sfManifestID, *other);
|
||||
view().insert(sleMan);
|
||||
};
|
||||
|
||||
write(klMan1, klMan2 ? std::optional<uint256>{klMan2->key} : std::nullopt);
|
||||
if (klMan2)
|
||||
write(*klMan2, klMan1.key);
|
||||
|
||||
sle->setFieldH256(sfManifestID, klMan1.key);
|
||||
view().update(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
SetManifest::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
XRPAmount manifestFee{0};
|
||||
if (tx.isFieldPresent(sfManifest))
|
||||
{
|
||||
STObject const& obj = const_cast<ripple::STTx&>(tx)
|
||||
.getField(sfManifest)
|
||||
.downcast<STObject>();
|
||||
|
||||
// one drop per byte
|
||||
manifestFee = XRPAmount{obj.getSerializer().getDataLength()};
|
||||
}
|
||||
|
||||
return Transactor::calculateBaseFee(view, tx) + manifestFee;
|
||||
}
|
||||
|
||||
/** The most sfFee may be: the same 1.2x headroom Submit applies.
|
||||
|
||||
Kept in one place so the value Submit writes and the value preclaim will
|
||||
accept cannot drift apart.
|
||||
*/
|
||||
static XRPAmount
|
||||
manifestFeeCeiling(XRPAmount baseFee)
|
||||
{
|
||||
return mulRatio(baseFee, 12, 10, /*roundUp*/ true);
|
||||
}
|
||||
|
||||
TER
|
||||
SetManifest::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
|
||||
{
|
||||
// A ceiling is required because the envelope carries no account signature,
|
||||
// so sfFee is chosen by whoever relays the txn -- and manifests are public:
|
||||
// they are gossiped over the peer protocol and embedded in published UNLs,
|
||||
// so the relayer need not be the master key holder. Uncapped, any observer
|
||||
// of a not-yet-recorded manifest could wrap it with sfFee set to that
|
||||
// validator's entire balance. The 20% band is headroom against a fee floor
|
||||
// that has risen since the txn was built, and bounds what an attacker can
|
||||
// burn to the same 20%.
|
||||
if (ctx.tx[sfFee].xrp() > manifestFeeCeiling(baseFee))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "SetManifest: fee above ceiling: "
|
||||
<< to_string(ctx.tx[sfFee].xrp());
|
||||
return temBAD_FEE;
|
||||
}
|
||||
|
||||
// Floor and balance are the ordinary rules.
|
||||
return Transactor::checkFee(ctx, baseFee);
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
makeSetManifestTx(
|
||||
Slice const& manifest,
|
||||
std::uint32_t networkID,
|
||||
ReadView const& openView,
|
||||
beast::Journal j)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto const man = deserializeManifest(manifest, j);
|
||||
if (!man || !man->verify())
|
||||
return std::nullopt;
|
||||
|
||||
auto const encode = [&](XRPAmount fee) {
|
||||
return serializeHex(STTx(ttMANIFEST_SET, [&](STObject& obj) {
|
||||
obj.setAccountID(sfAccount, calcAccountID(man->masterKey));
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
obj.setFieldU32(sfNetworkID, networkID);
|
||||
obj.setFieldAmount(sfFee, fee);
|
||||
obj.setFieldVL(sfSigningPubKey, std::vector<std::uint8_t>{});
|
||||
obj.setFieldVL(sfTxnSignature, std::vector<std::uint8_t>{});
|
||||
|
||||
// sfManifest is soeREQUIRED, so STObject::set(SOTemplate) has
|
||||
// already materialised it as a present, empty object. Fill
|
||||
// that one in: emitting a second is a duplicate field, which
|
||||
// STObject::set(SerialIter&) rejects on the way back in.
|
||||
SerialIter mit{manifest};
|
||||
obj.peekFieldObject(sfManifest).set(mit);
|
||||
}));
|
||||
};
|
||||
|
||||
// calculateBaseFee() takes a parsed transaction, so encode once with a
|
||||
// placeholder fee purely to have something to price. The resulting fee
|
||||
// does not depend on the placeholder: it is derived from the length of
|
||||
// the manifest object and the ledger's base fee.
|
||||
auto const priced = strUnHex(encode(XRPAmount{0}));
|
||||
if (!priced || priced->empty())
|
||||
return std::nullopt;
|
||||
|
||||
SerialIter sit{makeSlice(*priced)};
|
||||
STTx const probe{std::ref(sit)};
|
||||
|
||||
// Submit the ceiling exactly. preclaim rejects anything above it, and
|
||||
// the floor rises with network load, so the ceiling is both always
|
||||
// acceptable and the value most likely to still clear the floor by the
|
||||
// time the transaction is applied.
|
||||
return encode(
|
||||
manifestFeeCeiling(SetManifest::calculateBaseFee(openView, probe)));
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j.warn()) << "makeSetManifestTx: " << e.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,90 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2024 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_SETMANIFEST_H_INCLUDED
|
||||
#define RIPPLE_TX_SETMANIFEST_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Encode the transaction that publishes `manifest` on-ledger.
|
||||
|
||||
A manifest transaction carries no account signature, so the protocol pins
|
||||
the whole envelope: Sequence must be 0, SigningPubKey and TxnSignature must
|
||||
be empty, and Fee must fall between the computed base fee and a ceiling
|
||||
above it. SetManifest::preflight and SetManifest::checkFee reject anything
|
||||
else. Every caller that submits a manifest builds it here so those rules
|
||||
cannot drift apart from the ones the transactor enforces.
|
||||
|
||||
Returns hex rather than an STTx because the manifest is appended to the
|
||||
encoded transaction verbatim, behind its object marker, instead of being
|
||||
parsed and re-emitted: the bytes the master key signed survive untouched,
|
||||
and a future change to the manifest format needs no change here.
|
||||
|
||||
@param manifest Serialized manifest
|
||||
@param networkID Network the transaction is for
|
||||
@param openView Ledger the fee is priced against
|
||||
@param j Journal
|
||||
|
||||
@return the hex-encoded transaction, or nullopt if the manifest does not
|
||||
parse or does not verify
|
||||
*/
|
||||
std::optional<std::string>
|
||||
makeSetManifestTx(
|
||||
Slice const& manifest,
|
||||
std::uint32_t networkID,
|
||||
ReadView const& openView,
|
||||
beast::Journal j);
|
||||
|
||||
class SetManifest : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
|
||||
|
||||
explicit SetManifest(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
// Hides Transactor::checkFee; applySteps dispatches this as T::checkFee.
|
||||
static TER
|
||||
checkFee(PreclaimContext const& ctx, XRPAmount baseFee);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const&);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -604,6 +604,13 @@ Transactor::checkSeqProxy(
|
||||
return terNO_ACCOUNT;
|
||||
}
|
||||
|
||||
// A manifest txn is derived deterministically from the manifest alone, so
|
||||
// it cannot depend on account state: preflight pins sfSequence to 0 and the
|
||||
// account sequence is neither checked here nor consumed below.
|
||||
if (view.rules().enabled(featureOnChainManifests) &&
|
||||
tx.getTxnType() == ttMANIFEST_SET)
|
||||
return tesSUCCESS;
|
||||
|
||||
SeqProxy const a_seq = SeqProxy::sequence((*sle)[sfSequence]);
|
||||
|
||||
// pass all emitted tx provided their seq is 0
|
||||
@@ -755,6 +762,17 @@ Transactor::consumeSeqProxy(SLE::pointer const& sleAccount)
|
||||
if (ctx_.isEmittedTxn())
|
||||
return tesSUCCESS;
|
||||
|
||||
// Manifest txns get the same treatment: pinned to sfSequence 0 and not
|
||||
// signed by the account, so they neither consume nor reset its sequence.
|
||||
// Doing so would be actively harmful -- the write below is
|
||||
// seqProx.value() + 1, which for a seq-0 txn sets the account sequence to
|
||||
// 1 and makes every previously used sequence replayable. Handling it here
|
||||
// rather than in apply() also covers reset(), which re-consumes on the
|
||||
// tec / failed-invariant path.
|
||||
if (view().rules().enabled(featureOnChainManifests) &&
|
||||
ctx_.tx.getTxnType() == ttMANIFEST_SET)
|
||||
return tesSUCCESS;
|
||||
|
||||
SeqProxy const seqProx = ctx_.tx.getSeqProxy();
|
||||
if (seqProx.isSeq())
|
||||
{
|
||||
@@ -898,6 +916,12 @@ Transactor::checkSign(PreclaimContext const& ctx)
|
||||
ctx.tx.getTxnType() == ttIMPORT)
|
||||
return tesSUCCESS;
|
||||
|
||||
// pass ttMANIFEST_SETs, their signatures are checked in preflight against
|
||||
// the manifest's internal key logic
|
||||
if (ctx.view.rules().enabled(featureOnChainManifests) &&
|
||||
ctx.tx.getTxnType() == ttMANIFEST_SET)
|
||||
return tesSUCCESS;
|
||||
|
||||
if (ctx.flags & tapDRY_RUN)
|
||||
{
|
||||
// This code must be different for `simulate`
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/applySteps.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -73,6 +74,29 @@ checkValidity(
|
||||
return {Validity::Valid, ""};
|
||||
}
|
||||
|
||||
if (rules.enabled(featureOnChainManifests) &&
|
||||
tx.getTxnType() == ttMANIFEST_SET &&
|
||||
tx.isFieldPresent(sfTxnSignature) &&
|
||||
tx.getFieldVL(sfTxnSignature).empty() &&
|
||||
tx.isFieldPresent(sfSigningPubKey) &&
|
||||
tx.getFieldVL(sfSigningPubKey).empty() && tx.isFieldPresent(sfManifest))
|
||||
{
|
||||
// perform alternative signature check over manifest
|
||||
STObject const& manObj = const_cast<ripple::STTx&>(tx)
|
||||
.getField(sfManifest)
|
||||
.downcast<STObject>();
|
||||
|
||||
auto man = deserializeManifest(manObj);
|
||||
if (!man.has_value() || !man->verify())
|
||||
return {Validity::SigBad, "Manifest signature is bad"};
|
||||
|
||||
std::string reason;
|
||||
if (!passesLocalChecks(tx, reason))
|
||||
return {Validity::SigGoodOnly, reason};
|
||||
|
||||
return {Validity::Valid, ""};
|
||||
}
|
||||
|
||||
if (flags & SF_SIGBAD)
|
||||
// Signature is known bad
|
||||
return {Validity::SigBad, "Transaction has bad signature."};
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
#include <xrpld/app/tx/detail/Remit.h>
|
||||
#include <xrpld/app/tx/detail/SetAccount.h>
|
||||
#include <xrpld/app/tx/detail/SetHook.h>
|
||||
#include <xrpld/app/tx/detail/SetManifest.h>
|
||||
#include <xrpld/app/tx/detail/SetOracle.h>
|
||||
#include <xrpld/app/tx/detail/SetRegularKey.h>
|
||||
#include <xrpld/app/tx/detail/SetRemarks.h>
|
||||
|
||||
@@ -1190,7 +1190,13 @@ OverlayImpl::getManifestsMessage()
|
||||
{
|
||||
protocol::TMManifests tm;
|
||||
|
||||
app_.validatorManifests().for_each_manifest(
|
||||
// A bounded subset of the cache rather than all of it; see
|
||||
// ManifestCache::for_each_gossip_manifest for what is selected.
|
||||
// This message is only rebuilt when the cache sequence changes, so a
|
||||
// shift in which manifests are the most recently used does not by
|
||||
// itself refresh it. That is acceptable: the pinned manifests are the
|
||||
// ones a peer needs, and they are always included.
|
||||
app_.validatorManifests().for_each_gossip_manifest(
|
||||
[&tm](std::size_t s) { tm.mutable_list()->Reserve(s); },
|
||||
[&tm, &hr = app_.getHashRouter()](Manifest const& manifest) {
|
||||
tm.add_list()->set_stobject(
|
||||
|
||||
@@ -23,16 +23,197 @@
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/GRPCHandlers.h>
|
||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||
#include <xrpl/hook/Misc.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <algorithm>
|
||||
#include <grpc/status.h>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
namespace {
|
||||
|
||||
void
|
||||
setEffectiveParameters(
|
||||
STObject& result,
|
||||
STObject const& entry,
|
||||
std::shared_ptr<SLE const> const& definition)
|
||||
{
|
||||
bool const hasDefinition =
|
||||
definition && definition->isFieldPresent(sfHookParameters);
|
||||
bool const hasEntry = entry.isFieldPresent(sfHookParameters);
|
||||
if (!hasDefinition && !hasEntry)
|
||||
return; // LCOV_EXCL_LINE
|
||||
|
||||
STArray parameters{sfHookParameters};
|
||||
|
||||
auto merge = [¶meters](STArray const& source) {
|
||||
for (auto const& parameter : source)
|
||||
{
|
||||
Blob const name = parameter.getFieldVL(sfHookParameterName);
|
||||
Blob const value = parameter.getFieldVL(sfHookParameterValue);
|
||||
|
||||
auto existing = std::find_if(
|
||||
parameters.begin(),
|
||||
parameters.end(),
|
||||
[&name](STObject const& current) {
|
||||
return current.getFieldVL(sfHookParameterName) == name;
|
||||
});
|
||||
|
||||
if (existing == parameters.end())
|
||||
{
|
||||
STObject merged = STObject::makeInnerObject(sfHookParameter);
|
||||
merged.setFieldVL(sfHookParameterName, name);
|
||||
// gatherHookParameters treats an absent value as an empty
|
||||
// value. Keep it explicit in the effective response.
|
||||
merged.setFieldVL(sfHookParameterValue, value);
|
||||
parameters.push_back(std::move(merged));
|
||||
}
|
||||
else
|
||||
{
|
||||
existing->setFieldVL(sfHookParameterValue, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (hasDefinition)
|
||||
merge(definition->getFieldArray(sfHookParameters));
|
||||
if (hasEntry)
|
||||
merge(entry.getFieldArray(sfHookParameters));
|
||||
|
||||
result.setFieldArray(sfHookParameters, std::move(parameters));
|
||||
}
|
||||
|
||||
uint256
|
||||
effectiveHookOn(
|
||||
STObject const& entry,
|
||||
std::shared_ptr<SLE const> const& definition,
|
||||
SField const& direction)
|
||||
{
|
||||
if (entry.isFieldPresent(direction))
|
||||
return entry.getFieldH256(direction);
|
||||
if (entry.isFieldPresent(sfHookOn))
|
||||
return entry.getFieldH256(sfHookOn);
|
||||
|
||||
if (!definition)
|
||||
return uint256{0}; // LCOV_EXCL_LINE
|
||||
|
||||
if (definition->isFieldPresent(direction))
|
||||
return definition->getFieldH256(direction);
|
||||
if (definition->isFieldPresent(sfHookOn))
|
||||
return definition->getFieldH256(sfHookOn);
|
||||
return uint256{0}; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
STObject
|
||||
effectiveHook(
|
||||
STObject const& entry,
|
||||
std::shared_ptr<SLE const> const& definition)
|
||||
{
|
||||
STObject result = STObject::makeInnerObject(sfHook);
|
||||
|
||||
// If there is no definition, return the entry as is
|
||||
if (!definition)
|
||||
return result; // LCOV_EXCL_LINE
|
||||
|
||||
// A blank sfHooks slot is meaningful: preserve it as an empty Hook
|
||||
// object instead of applying the defaults used for an installed hook.
|
||||
if (!entry.isFieldPresent(sfHookHash))
|
||||
return result; // LCOV_EXCL_LINE
|
||||
|
||||
// These fields are deliberately selected rather than copying the raw
|
||||
// objects: HookDefinition bookkeeping must not leak from account_info.
|
||||
if (entry.isFieldPresent(sfHookHash))
|
||||
result.setFieldH256(sfHookHash, entry.getFieldH256(sfHookHash));
|
||||
// CreateCode is not included in AccountRoot Hook field
|
||||
// if (definition->isFieldPresent(sfCreateCode))
|
||||
// result.setFieldVL(sfCreateCode,
|
||||
// definition->getFieldVL(sfCreateCode));
|
||||
if (entry.isFieldPresent(sfHookGrants))
|
||||
result.setFieldArray(sfHookGrants, entry.getFieldArray(sfHookGrants));
|
||||
|
||||
if (entry.isFieldPresent(sfHookNamespace))
|
||||
result.setFieldH256(
|
||||
sfHookNamespace, entry.getFieldH256(sfHookNamespace));
|
||||
else if (definition->isFieldPresent(sfHookNamespace))
|
||||
result.setFieldH256(
|
||||
sfHookNamespace, definition->getFieldH256(sfHookNamespace));
|
||||
|
||||
if (definition->isFieldPresent(sfHookApiVersion))
|
||||
result.setFieldU16(
|
||||
sfHookApiVersion, definition->getFieldU16(sfHookApiVersion));
|
||||
|
||||
if (entry.isFieldPresent(sfHookCanEmit))
|
||||
result.setFieldH256(sfHookCanEmit, entry.getFieldH256(sfHookCanEmit));
|
||||
else if (definition->isFieldPresent(sfHookCanEmit))
|
||||
result.setFieldH256(
|
||||
sfHookCanEmit, definition->getFieldH256(sfHookCanEmit));
|
||||
else
|
||||
result.setFieldH256(sfHookCanEmit, UINT256_BIT[ttHOOK_SET]);
|
||||
|
||||
if (entry.isFieldPresent(sfHookName))
|
||||
result.setFieldVL(sfHookName, entry.getFieldVL(sfHookName));
|
||||
if (entry.isFieldPresent(sfFlags))
|
||||
result.setFieldU32(sfFlags, entry.getFieldU32(sfFlags));
|
||||
else if (definition->isFieldPresent(sfFlags))
|
||||
result.setFieldU32(sfFlags, definition->getFieldU32(sfFlags));
|
||||
|
||||
auto const incoming = effectiveHookOn(entry, definition, sfHookOnIncoming);
|
||||
auto const outgoing = effectiveHookOn(entry, definition, sfHookOnOutgoing);
|
||||
if (incoming == outgoing)
|
||||
result.setFieldH256(sfHookOn, incoming);
|
||||
else
|
||||
{
|
||||
result.setFieldH256(sfHookOnIncoming, incoming);
|
||||
result.setFieldH256(sfHookOnOutgoing, outgoing);
|
||||
}
|
||||
|
||||
setEffectiveParameters(result, entry, definition);
|
||||
return result;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
accountHooks(ReadView const& ledger, AccountID const& account)
|
||||
{
|
||||
Json::Value hooks(Json::arrayValue);
|
||||
auto const hookSLE = ledger.read(keylet::hook(account));
|
||||
if (!hookSLE || !hookSLE->isFieldPresent(sfHooks))
|
||||
return hooks;
|
||||
|
||||
std::map<uint256, std::shared_ptr<SLE const>> definitions;
|
||||
for (auto const& hookElement : hookSLE->getFieldArray(sfHooks))
|
||||
{
|
||||
auto const& entry = hookElement.downcast<STObject const>();
|
||||
std::shared_ptr<SLE const> definition;
|
||||
if (entry.isFieldPresent(sfHookHash))
|
||||
{
|
||||
auto const hash = entry.getFieldH256(sfHookHash);
|
||||
auto [it, inserted] = definitions.emplace(hash, nullptr);
|
||||
if (inserted)
|
||||
it->second = ledger.read(keylet::hookDefinition(hash));
|
||||
definition = it->second;
|
||||
}
|
||||
|
||||
Json::Value wrapped(Json::objectValue);
|
||||
wrapped[sfHook.jsonName] =
|
||||
effectiveHook(entry, definition).getJson(JsonOptions::none);
|
||||
hooks.append(std::move(wrapped));
|
||||
}
|
||||
return hooks;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// {
|
||||
// account: <ident>,
|
||||
// ledger_hash : <ledger>
|
||||
@@ -52,6 +233,11 @@ doAccountInfo(RPC::JsonContext& context)
|
||||
{
|
||||
auto& params = context.params;
|
||||
|
||||
if (params.isMember(jss::hooks) && !params[jss::hooks].isBool())
|
||||
return RPC::invalid_field_error(jss::hooks);
|
||||
bool const includeHooks =
|
||||
params.isMember(jss::hooks) && params[jss::hooks].asBool();
|
||||
|
||||
std::string strIdent;
|
||||
if (params.isMember(jss::account))
|
||||
{
|
||||
@@ -146,6 +332,9 @@ doAccountInfo(RPC::JsonContext& context)
|
||||
|
||||
result[jss::account_flags] = std::move(acctFlags);
|
||||
|
||||
if (includeHooks)
|
||||
result[jss::hooks] = accountHooks(*ledger, accountID);
|
||||
|
||||
// The document[https://xrpl.org/account_info.html#account_info] states
|
||||
// that signer_lists is a bool, however assigning any string value
|
||||
// works. Do not allow this. This check is for api Version 2 onwards
|
||||
|
||||
@@ -18,15 +18,20 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/Transaction.h>
|
||||
#include <xrpld/app/misc/TxQ.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/detail/SetManifest.h> // makeSetManifestTx
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/GRPCHandlers.h>
|
||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||
#include <xrpld/rpc/detail/TransactionSign.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
@@ -89,9 +94,20 @@ doInject(RPC::JsonContext& context)
|
||||
Json::Value
|
||||
doSubmit(RPC::JsonContext& context)
|
||||
{
|
||||
Json::Value jvResult;
|
||||
|
||||
context.loadType = Resource::feeMediumBurdenRPC;
|
||||
|
||||
if (!context.params.isMember(jss::tx_blob))
|
||||
bool const hasManifest = context.params.isMember(jss::manifest);
|
||||
bool const hasTxBlob = context.params.isMember(jss::tx_blob);
|
||||
|
||||
if (hasManifest && hasTxBlob)
|
||||
{
|
||||
return RPC::make_error(
|
||||
rpcINVALID_PARAMS,
|
||||
"Specify exactly one of either `tx_blob` or `manifest`");
|
||||
}
|
||||
else if (!hasTxBlob && !hasManifest)
|
||||
{
|
||||
auto const failType = getFailHard(context);
|
||||
|
||||
@@ -117,9 +133,49 @@ doSubmit(RPC::JsonContext& context)
|
||||
return ret;
|
||||
}
|
||||
|
||||
Json::Value jvResult;
|
||||
std::string txBlob =
|
||||
hasTxBlob ? context.params[jss::tx_blob].asString() : "";
|
||||
|
||||
auto ret = strUnHex(context.params[jss::tx_blob].asString());
|
||||
if (hasManifest)
|
||||
{
|
||||
// OnChainManifests amendment accepts a manifest submission here; turn
|
||||
// it into the transaction that carries it and drop through to normal
|
||||
// tx_blob processing below.
|
||||
auto const view = context.app.openLedger().current();
|
||||
|
||||
// The transaction built below carries no account signature; its
|
||||
// authority is the manifest's own master and ephemeral signatures,
|
||||
// which checkValidity() only honours once the amendment is active.
|
||||
// Without this the submitter is told their transaction is unsigned,
|
||||
// which reads as their mistake. It isn't -- the feature is not live
|
||||
// yet -- so say so before touching the manifest at all.
|
||||
if (!view->rules().enabled(featureOnChainManifests))
|
||||
return RPC::make_error(
|
||||
rpcNOT_ENABLED,
|
||||
"The OnChainManifests amendment is not enabled on this "
|
||||
"network. Manifest submission will work once it activates; "
|
||||
"nothing is wrong with this request.");
|
||||
|
||||
auto const raw = strUnHex(context.params[jss::manifest].asString());
|
||||
if (!raw || raw->empty())
|
||||
return rpcError(rpcINVALID_PARAMS);
|
||||
|
||||
auto const hex = makeSetManifestTx(
|
||||
makeSlice(*raw),
|
||||
context.app.config().NETWORK_ID,
|
||||
*view,
|
||||
context.app.journal("Submit"));
|
||||
|
||||
if (!hex)
|
||||
{
|
||||
jvResult[jss::error] = "invalidManifest";
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
txBlob = *hex;
|
||||
}
|
||||
|
||||
auto ret = strUnHex(txBlob);
|
||||
|
||||
if (!ret || !ret->size())
|
||||
return rpcError(rpcINVALID_PARAMS);
|
||||
|
||||
Reference in new issue
Block a user