diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj
index dace055e2f7..8813214846f 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj
+++ b/Builds/VisualStudio2015/RippleD.vcxproj
@@ -4931,6 +4931,10 @@
True
True
+
+ True
+ True
+
True
True
diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters
index f23bc5e24e0..728aab766a1 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj.filters
+++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters
@@ -5646,6 +5646,9 @@
test\rpc
+
+ test\rpc
+
test\rpc
diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp
index b26451201d8..4a4cfcb7716 100644
--- a/src/ripple/app/consensus/RCLConsensus.cpp
+++ b/src/ripple/app/consensus/RCLConsensus.cpp
@@ -910,9 +910,12 @@ RCLConsensus::startRound(
RCLCxLedger::ID const& prevLgrId,
RCLCxLedger const& prevLgr)
{
- // We have a key, and we have some idea what the ledger is
+ // We have a key, we have some idea what the ledger is, and we are not
+ // amendment blocked
validating_ =
- !app_.getOPs().isNeedNetworkLedger() && (valPublic_.size() != 0);
+ !app_.getOPs().isNeedNetworkLedger() &&
+ (valPublic_.size() != 0) &&
+ !app_.getOPs().isAmendmentBlocked();
// propose only if we're in sync with the network (and validating)
bool proposing =
diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp
index 6acb51b02fc..5deed8de771 100644
--- a/src/ripple/app/ledger/impl/LedgerMaster.cpp
+++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp
@@ -227,6 +227,13 @@ LedgerMaster::setValidLedger(
app_.getSHAMapStore().onLedgerClosed (getValidatedLedger());
mLedgerHistory.validatedLedger (l);
app_.getAmendmentTable().doValidatedLedger (l);
+ if (!app_.getOPs().isAmendmentBlocked() &&
+ app_.getAmendmentTable().hasUnsupportedEnabled ())
+ {
+ JLOG (m_journal.error()) <<
+ "One or more unsupported amendments activated: server blocked.";
+ app_.getOPs().setAmendmentBlocked();
+ }
}
void
diff --git a/src/ripple/app/misc/AmendmentTable.h b/src/ripple/app/misc/AmendmentTable.h
index 2cad2343b0d..674fd57bbcd 100644
--- a/src/ripple/app/misc/AmendmentTable.h
+++ b/src/ripple/app/misc/AmendmentTable.h
@@ -47,6 +47,14 @@ class AmendmentTable
virtual bool isEnabled (uint256 const& amendment) = 0;
virtual bool isSupported (uint256 const& amendment) = 0;
+ /**
+ * @brief returns true if one or more amendments on the network
+ * have been enabled that this server does not support
+ *
+ * @return true if an unsupported feature is enabled on the network
+ */
+ virtual bool hasUnsupportedEnabled () = 0;
+
virtual Json::Value getJson (int) = 0;
/** Returns a Json::objectValue. */
diff --git a/src/ripple/app/misc/impl/AmendmentTable.cpp b/src/ripple/app/misc/impl/AmendmentTable.cpp
index 9be2cb18316..bb6d3ba1d0e 100644
--- a/src/ripple/app/misc/impl/AmendmentTable.cpp
+++ b/src/ripple/app/misc/impl/AmendmentTable.cpp
@@ -157,6 +157,9 @@ class AmendmentTableImpl final
// we haven't participated in one yet.
std::unique_ptr lastVote_;
+ // True if an unsupported amendment is enabled
+ bool unsupportedEnabled_;
+
beast::Journal j_;
// Finds or creates state
@@ -187,6 +190,8 @@ class AmendmentTableImpl final
bool isEnabled (uint256 const& amendment) override;
bool isSupported (uint256 const& amendment) override;
+ bool hasUnsupportedEnabled () override;
+
Json::Value getJson (int) override;
Json::Value getJson (uint256 const&) override;
@@ -222,6 +227,7 @@ AmendmentTableImpl::AmendmentTableImpl (
: lastUpdateSeq_ (0)
, majorityTime_ (majorityTime)
, majorityFraction_ (majorityFraction)
+ , unsupportedEnabled_ (false)
, j_ (journal)
{
assert (majorityFraction_ != 0);
@@ -340,6 +346,14 @@ AmendmentTableImpl::enable (uint256 const& amendment)
return false;
s->enabled = true;
+
+ if (! s->supported)
+ {
+ JLOG (j_.error()) <<
+ "Unsupported amendment " << amendment << " activated.";
+ unsupportedEnabled_ = true;
+ }
+
return true;
}
@@ -372,6 +386,13 @@ AmendmentTableImpl::isSupported (uint256 const& amendment)
return s && s->supported;
}
+bool
+AmendmentTableImpl::hasUnsupportedEnabled ()
+{
+ std::lock_guard sl (mutex_);
+ return unsupportedEnabled_;
+}
+
std::vector
AmendmentTableImpl::doValidation (
std::set const& enabled)
@@ -523,10 +544,8 @@ AmendmentTableImpl::doValidatedLedger (
LedgerIndex ledgerSeq,
std::set const& enabled)
{
- std::lock_guard sl (mutex_);
-
- for (auto& e : amendmentMap_)
- e.second.enabled = (enabled.count (e.first) != 0);
+ for (auto& e : enabled)
+ enable(e);
}
void
diff --git a/src/ripple/protocol/ErrorCodes.h b/src/ripple/protocol/ErrorCodes.h
index e9e31f3ac29..c758ace81ca 100644
--- a/src/ripple/protocol/ErrorCodes.h
+++ b/src/ripple/protocol/ErrorCodes.h
@@ -51,6 +51,7 @@ enum error_code_i
rpcHIGH_FEE,
rpcNOT_ENABLED,
rpcNOT_READY,
+ rpcAMENDMENT_BLOCKED,
// Networking
rpcNO_CLOSED,
diff --git a/src/ripple/protocol/impl/ErrorCodes.cpp b/src/ripple/protocol/impl/ErrorCodes.cpp
index 4a92538728f..d0d1e154ac7 100644
--- a/src/ripple/protocol/impl/ErrorCodes.cpp
+++ b/src/ripple/protocol/impl/ErrorCodes.cpp
@@ -53,6 +53,7 @@ class ErrorCategory
add (rpcACT_EXISTS, "actExists", "Account already exists.");
add (rpcACT_MALFORMED, "actMalformed", "Account malformed.");
add (rpcACT_NOT_FOUND, "actNotFound", "Account not found.");
+ add (rpcAMENDMENT_BLOCKED, "amendmentBlocked", "Amendment blocked, need upgrade.");
add (rpcATX_DEPRECATED, "deprecated", "Use the new API or specify a ledger range.");
add (rpcBAD_BLOB, "badBlob", "Blob must be a non-empty hex string.");
add (rpcBAD_FEATURE, "badFeature", "Feature unknown or invalid.");
diff --git a/src/ripple/rpc/impl/RPCHandler.cpp b/src/ripple/rpc/impl/RPCHandler.cpp
index 6b0ba384374..5151af425bb 100644
--- a/src/ripple/rpc/impl/RPCHandler.cpp
+++ b/src/ripple/rpc/impl/RPCHandler.cpp
@@ -157,6 +157,13 @@ error_code_i fillHandler (Context& context,
return rpcNO_NETWORK;
}
+ if (context.app.getOPs().isAmendmentBlocked() &&
+ (handler->condition_ & NEEDS_CURRENT_LEDGER ||
+ handler->condition_ & NEEDS_CLOSED_LEDGER))
+ {
+ return rpcAMENDMENT_BLOCKED;
+ }
+
if (!context.app.config().standalone() &&
handler->condition_ & NEEDS_CURRENT_LEDGER)
{
diff --git a/src/test/app/AmendmentTable_test.cpp b/src/test/app/AmendmentTable_test.cpp
index d61a7fa6335..e2bc0bc1516 100644
--- a/src/test/app/AmendmentTable_test.cpp
+++ b/src/test/app/AmendmentTable_test.cpp
@@ -745,6 +745,20 @@ class AmendmentTable_test final : public beast::unit_test::suite
}
}
+ void testHasUnsupported ()
+ {
+ testcase ("hasUnsupportedEnabled");
+
+ auto table = makeTable(1);
+ BEAST_EXPECT(! table->hasUnsupportedEnabled());
+
+ std::set enabled;
+ std::for_each(m_set4.begin(), m_set4.end(),
+ [&enabled](auto const &s){ enabled.insert(amendmentId(s)); });
+ table->doValidatedLedger(1, enabled);
+ BEAST_EXPECT(table->hasUnsupportedEnabled());
+ }
+
void run ()
{
testConstruct();
@@ -757,6 +771,7 @@ class AmendmentTable_test final : public beast::unit_test::suite
testDetectMajority ();
testLostMajority ();
testSupportedAmendments ();
+ testHasUnsupported ();
}
};
diff --git a/src/test/rpc/AmendmentBlocked_test.cpp b/src/test/rpc/AmendmentBlocked_test.cpp
new file mode 100644
index 00000000000..cd68e76bab9
--- /dev/null
+++ b/src/test/rpc/AmendmentBlocked_test.cpp
@@ -0,0 +1,169 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2017 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
+#include
+#include
+#include
+
+namespace ripple {
+
+class AmendmentBlocked_test : public beast::unit_test::suite
+{
+ void testBlockedMethods()
+ {
+ using namespace test::jtx;
+ Env env {*this};
+ auto const gw = Account {"gateway"};
+ auto const USD = gw["USD"];
+ auto const alice = Account {"alice"};
+ auto const bob = Account {"bob"};
+ env.fund (XRP(10000), alice, bob, gw);
+ env.trust (USD(600), alice);
+ env.trust (USD(700), bob);
+ env(pay (gw, alice, USD(70)));
+ env(pay (gw, bob, USD(50)));
+ env.close();
+
+ auto wsc = test::makeWSClient(env.app().config());
+
+ auto current = env.current ();
+ // ledger_accept
+ auto jr = env.rpc ("ledger_accept") [jss::result];
+ BEAST_EXPECT (jr[jss::ledger_current_index] == current->seq ()+1);
+
+ // ledger_current
+ jr = env.rpc ("ledger_current") [jss::result];
+ BEAST_EXPECT (jr[jss::ledger_current_index] == current->seq ()+1);
+
+ // owner_info
+ jr = env.rpc ("owner_info", alice.human()) [jss::result];
+ BEAST_EXPECT (jr.isMember (jss::accepted) && jr.isMember (jss::current));
+
+ // path_find
+ Json::Value pf_req;
+ pf_req[jss::subcommand] = "create";
+ pf_req[jss::source_account] = alice.human();
+ pf_req[jss::destination_account] = bob.human();
+ pf_req[jss::destination_amount] = bob["USD"](20).value ().getJson (0);
+ jr = wsc->invoke("path_find", pf_req) [jss::result];
+ BEAST_EXPECT (jr.isMember (jss::alternatives) &&
+ jr[jss::alternatives].isArray () &&
+ jr[jss::alternatives].size () == 1);
+
+ // submit
+ auto jt = env.jt (noop (alice));
+ Serializer s;
+ jt.stx->add (s);
+ jr = env.rpc ("submit", strHex (s.slice ())) [jss::result];
+ BEAST_EXPECT (jr.isMember (jss::engine_result) &&
+ jr[jss::engine_result] == "tesSUCCESS");
+
+ // submit_multisigned
+ env(signers(bob, 1, {{alice, 1}}), sig (bob));
+ Account const ali {"ali", KeyType::secp256k1};
+ env(regkey (alice, ali));
+ env.close();
+
+ Json::Value set_tx;
+ set_tx[jss::Account] = bob.human();
+ set_tx[jss::TransactionType] = "AccountSet";
+ set_tx[jss::Fee] =
+ static_cast(8 * env.current()->fees().base);
+ set_tx[jss::Sequence] = env.seq(bob);
+ set_tx[jss::SigningPubKey] = "";
+
+ Json::Value sign_for;
+ sign_for[jss::tx_json] = set_tx;
+ sign_for[jss::account] = alice.human();
+ sign_for[jss::secret] = ali.name();
+ jr = env.rpc("json", "sign_for", to_string(sign_for)) [jss::result];
+ BEAST_EXPECT(jr[jss::status] == "success");
+
+ Json::Value ms_req;
+ ms_req[jss::tx_json] = jr[jss::tx_json];
+ jr = env.rpc("json", "submit_multisigned", to_string(ms_req))
+ [jss::result];
+ BEAST_EXPECT (jr.isMember (jss::engine_result) &&
+ jr[jss::engine_result] == "tesSUCCESS");
+
+ // make the network amendment blocked...now all the same
+ // requests should fail
+
+ env.app ().getOPs ().setAmendmentBlocked ();
+
+ // ledger_accept
+ jr = env.rpc ("ledger_accept") [jss::result];
+ BEAST_EXPECT(
+ jr.isMember (jss::error) &&
+ jr[jss::error] == "amendmentBlocked");
+ BEAST_EXPECT(jr[jss::status] == "error");
+
+ // ledger_current
+ jr = env.rpc ("ledger_current") [jss::result];
+ BEAST_EXPECT(
+ jr.isMember (jss::error) &&
+ jr[jss::error] == "amendmentBlocked");
+ BEAST_EXPECT(jr[jss::status] == "error");
+
+ // owner_info
+ jr = env.rpc ("owner_info", alice.human()) [jss::result];
+ BEAST_EXPECT(
+ jr.isMember (jss::error) &&
+ jr[jss::error] == "amendmentBlocked");
+ BEAST_EXPECT(jr[jss::status] == "error");
+
+ // path_find
+ jr = wsc->invoke("path_find", pf_req) [jss::result];
+ BEAST_EXPECT(
+ jr.isMember (jss::error) &&
+ jr[jss::error] == "amendmentBlocked");
+ BEAST_EXPECT(jr[jss::status] == "error");
+
+ // submit
+ jr = env.rpc("submit", strHex(s.slice())) [jss::result];
+ BEAST_EXPECT(
+ jr.isMember (jss::error) &&
+ jr[jss::error] == "amendmentBlocked");
+ BEAST_EXPECT(jr[jss::status] == "error");
+
+ // submit_multisigned
+ set_tx[jss::Sequence] = env.seq(bob);
+ sign_for[jss::tx_json] = set_tx;
+ jr = env.rpc("json", "sign_for", to_string(sign_for)) [jss::result];
+ BEAST_EXPECT(jr[jss::status] == "success");
+ ms_req[jss::tx_json] = jr[jss::tx_json];
+ jr = env.rpc("json", "submit_multisigned", to_string(ms_req))
+ [jss::result];
+ BEAST_EXPECT(
+ jr.isMember (jss::error) &&
+ jr[jss::error] == "amendmentBlocked");
+ }
+
+public:
+ void run()
+ {
+ testBlockedMethods();
+ }
+};
+
+BEAST_DEFINE_TESTSUITE(AmendmentBlocked,app,ripple);
+
+}
+
diff --git a/src/test/server/ServerStatus_test.cpp b/src/test/server/ServerStatus_test.cpp
index 4ac941cdadc..8e214642f82 100644
--- a/src/test/server/ServerStatus_test.cpp
+++ b/src/test/server/ServerStatus_test.cpp
@@ -25,6 +25,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -87,7 +89,7 @@ class ServerStatus_test :
req.target("/");
req.version = 11;
- req.insert("Host", host + ":" + to_string(port));
+ req.insert("Host", host + ":" + std::to_string(port));
req.insert("User-Agent", "test");
req.method(beast::http::verb::get);
req.insert("Upgrade", "websocket");
@@ -111,7 +113,7 @@ class ServerStatus_test :
req.target("/");
req.version = 11;
- req.insert("Host", host + ":" + to_string(port));
+ req.insert("Host", host + ":" + std::to_string(port));
req.insert("User-Agent", "test");
if(body.empty())
{
@@ -131,7 +133,7 @@ class ServerStatus_test :
void
doRequest(
boost::asio::yield_context& yield,
- beast::http::request& req,
+ beast::http::request&& req,
std::string const& host,
uint16_t port,
bool secure,
@@ -146,7 +148,7 @@ class ServerStatus_test :
auto it =
r.async_resolve(
- ip::tcp::resolver::query{host, to_string(port)}, yield[ec]);
+ ip::tcp::resolver::query{host, std::to_string(port)}, yield[ec]);
if(ec)
return;
@@ -197,9 +199,14 @@ class ServerStatus_test :
get("port");
auto ip = env.app().config()["port_ws"].
get("ip");
- auto req = makeWSUpgrade(*ip, *port);
doRequest(
- yield, req, *ip, *port, secure, resp, ec);
+ yield,
+ makeWSUpgrade(*ip, *port),
+ *ip,
+ *port,
+ secure,
+ resp,
+ ec);
return;
}
@@ -216,10 +223,9 @@ class ServerStatus_test :
get("port");
auto const ip = env.app().config()["port_rpc"].
get("ip");
- auto req = makeHTTPRequest(*ip, *port, body);
doRequest(
yield,
- req,
+ makeHTTPRequest(*ip, *port, body),
*ip,
*port,
secure,
@@ -368,7 +374,7 @@ class ServerStatus_test :
auto it =
r.async_resolve(
- ip::tcp::resolver::query{*ip, to_string(*port)}, yield[ec]);
+ ip::tcp::resolver::query{*ip, std::to_string(*port)}, yield[ec]);
if(! BEAST_EXPECTS(! ec, ec.message()))
return;
@@ -510,6 +516,110 @@ class ServerStatus_test :
}
}
+ void
+ testAmendmentBlock(boost::asio::yield_context& yield)
+ {
+ testcase("Status request over WS and RPC with/without Amendment Block");
+ using namespace jtx;
+ using namespace boost::asio;
+ using namespace beast::http;
+ Env env {*this, validator( envconfig([](std::unique_ptr cfg)
+ {
+ cfg->section("port_rpc").set("protocol", "http,https");
+ return cfg;
+ }), "")};
+
+ env.close();
+
+ // advance the ledger so that server status
+ // sees a published ledger -- without this, we get a status
+ // failure message about no published ledgers
+ env.app().getLedgerMaster().tryAdvance();
+
+ // make an RPC server info request and look for
+ // amendment_blocked status
+ auto si = env.rpc("server_info") [jss::result];
+ BEAST_EXPECT(! si[jss::info].isMember(jss::amendment_blocked));
+ BEAST_EXPECT(
+ env.app().getOPs().getConsensusInfo()["validating"] == true);
+
+ auto const port_ws = env.app().config()["port_ws"].
+ get("port");
+ auto const ip_ws = env.app().config()["port_ws"].
+ get("ip");
+
+
+ boost::system::error_code ec;
+ response resp;
+
+ doRequest(
+ yield,
+ makeHTTPRequest(*ip_ws, *port_ws, ""),
+ *ip_ws,
+ *port_ws,
+ false,
+ resp,
+ ec);
+
+ if(! BEAST_EXPECTS(! ec, ec.message()))
+ return;
+ BEAST_EXPECT(resp.result() == beast::http::status::ok);
+ BEAST_EXPECT(
+ resp.body.find("connectivity is working.") != std::string::npos);
+
+ // mark the Network as Amendment Blocked, but still won't fail until
+ // ELB is enabled (next step)
+ env.app().getOPs().setAmendmentBlocked ();
+ env.app().getOPs().beginConsensus(env.closed()->info().hash);
+
+ // consensus now sees validation disabled
+ BEAST_EXPECT(
+ env.app().getOPs().getConsensusInfo()["validating"] == false);
+
+ // RPC request server_info again, now AB should be returned
+ si = env.rpc("server_info") [jss::result];
+ BEAST_EXPECT(
+ si[jss::info].isMember(jss::amendment_blocked) &&
+ si[jss::info][jss::amendment_blocked] == true);
+
+ // but status does not indicate because it still relies on ELB
+ // being enabled
+ doRequest(
+ yield,
+ makeHTTPRequest(*ip_ws, *port_ws, ""),
+ *ip_ws,
+ *port_ws,
+ false,
+ resp,
+ ec);
+
+ if(! BEAST_EXPECTS(! ec, ec.message()))
+ return;
+ BEAST_EXPECT(resp.result() == beast::http::status::ok);
+ BEAST_EXPECT(
+ resp.body.find("connectivity is working.") != std::string::npos);
+
+ env.app().config().ELB_SUPPORT = true;
+
+ doRequest(
+ yield,
+ makeHTTPRequest(*ip_ws, *port_ws, ""),
+ *ip_ws,
+ *port_ws,
+ false,
+ resp,
+ ec);
+
+ if(! BEAST_EXPECTS(! ec, ec.message()))
+ return;
+ BEAST_EXPECT(resp.result() == beast::http::status::internal_server_error);
+ BEAST_EXPECT(
+ resp.body.find("cannot accept clients:") != std::string::npos);
+ BEAST_EXPECT(
+ resp.body.find("Server version too old") != std::string::npos);
+
+ }
+
public:
void
run()
@@ -527,6 +637,7 @@ class ServerStatus_test :
//THIS HANGS - testCantConnect("wss", "ws", yield);
testCantConnect("wss2", "ws2", yield);
testCantConnect("https", "http", yield);
+ testAmendmentBlock(yield);
});
for (auto it : {"http", "ws", "ws2"})
diff --git a/src/test/unity/rpc_test_unity.cpp b/src/test/unity/rpc_test_unity.cpp
index 181e845a423..3431d8bc22b 100644
--- a/src/test/unity/rpc_test_unity.cpp
+++ b/src/test/unity/rpc_test_unity.cpp
@@ -24,6 +24,7 @@
#include
#include
#include
+#include
#include
#include
#include