Compare commits

...
Author SHA1 Message Date
Nicholas Dudfield 6238cb7109 test(amendment): cover unsupported ledger jump shutdown 2026-09-07 18:49:31 +07:00
Richard Holland 0cd4f6aae4 test cases and more useful message 2026-09-07 19:40:52 +10:00
Richard Holland a4b0a90998 Merge branch 'dev' into fail-fast-amendment-blocking 2026-09-07 13:25:17 +10:00
Richard Holland 6191c1e09d Onchain manifests (#779) 2026-09-07 13:19:25 +10:00
Nicholas Dudfield 6981b5b06b refactor(app): colocate amendment receipt helpers 2026-09-02 16:35:50 +07:00
tequ f1a3a775a1 Add optional Hooks data to account_info response (#789) 2026-09-02 17:22:53 +10:00
Nicholas Dudfield d4a681430a fix(amendment): persist blocked shutdown receipt 2026-09-02 13:44:53 +07:00
Nicholas Dudfield 3760e1fb41 fix: begin unsupported amendment shutdown five minutes early 2026-09-01 13:06:13 +07:00
tequ c1dabe5576 Fix comments (#768) 2026-09-01 14:18:28 +10:00
Nicholas Dudfield 6442e0990b Merge remote-tracking branch 'origin/dev' into fail-fast-amendment-blocking 2026-09-01 09:41:41 +07:00
Nicholas Dudfield 1e6cda4d64 fix: add fatal log on amendment-blocked shutdown 2026-06-19 09:35:39 +07:00
Nicholas Dudfield c146f15247 fix: rethrow runtime_error if not amendment blocked 2026-06-19 09:35:39 +07:00
Nicholas Dudfield fb5081d1f4 fix: narrow catch to std::runtime_error in switchLastClosedLedger 2026-06-19 09:35:39 +07:00
Nicholas Dudfield d4bec012a2 fix: skip signalStop in standalone mode for test compatibility 2026-06-19 09:35:39 +07:00
Nicholas Dudfield f7187ba94f fix: fail fast when amendment blocked instead of zombie state
- signalStop() for graceful shutdown when unsupported amendment activates
- early shutdown ~1 minute before expected activation to avoid race
- try/catch in switchLastClosedLedger to survive unknown field crashes
  during shutdown window
- show amendment warning to all RPC users, not just admin

Fixes: #706
2026-06-19 09:35:39 +07:00
40 changed files with 3466 additions and 20 deletions

No files matched your search

+1
View File
@@ -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
+2 -2
View File
@@ -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>
#
+2
View File
@@ -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)
+1
View File
@@ -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
+1 -1
View File
@@ -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
};
+4
View File
@@ -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:
+2
View File
@@ -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},
+1
View File
@@ -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
+7
View File
@@ -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
+2
View File
@@ -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."),
+147
View File
@@ -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)
{
+819
View File
@@ -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
+250
View File
@@ -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();
+543
View File
@@ -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);
+22
View File
@@ -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:
+37 -3
View File
@@ -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();
+60
View File
@@ -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,
+26
View File
@@ -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
+25
View File
@@ -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())
+18
View File
@@ -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;
+215
View File
@@ -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
+200 -4
View File
@@ -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;
}
+173 -1
View File
@@ -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));
+12 -2
View File
@@ -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);
+5 -3
View File
@@ -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;
+404
View File
@@ -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
+90
View File
@@ -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
+24
View File
@@ -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`
+24
View File
@@ -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."};
+1
View File
@@ -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>
+7 -1
View File
@@ -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(
+189
View File
@@ -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 = [&parameters](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
+59 -3
View File
@@ -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);