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