diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index e0c627e2bd..b1cf2557a0 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -407,6 +407,17 @@ target_sources (rippled PRIVATE src/ripple/app/rdb/impl/RelationalDBInterface_nodes.cpp src/ripple/app/rdb/impl/RelationalDBInterface_postgres.cpp src/ripple/app/rdb/impl/RelationalDBInterface_shards.cpp + src/ripple/app/sidechain/Federator.cpp + src/ripple/app/sidechain/FederatorEvents.cpp + src/ripple/app/sidechain/impl/ChainListener.cpp + src/ripple/app/sidechain/impl/DoorKeeper.cpp + src/ripple/app/sidechain/impl/InitialSync.cpp + src/ripple/app/sidechain/impl/MainchainListener.cpp + src/ripple/app/sidechain/impl/SidechainListener.cpp + src/ripple/app/sidechain/impl/SignatureCollector.cpp + src/ripple/app/sidechain/impl/SignerList.cpp + src/ripple/app/sidechain/impl/TicketHolder.cpp + src/ripple/app/sidechain/impl/WebsocketClient.cpp src/ripple/app/tx/impl/ApplyContext.cpp src/ripple/app/tx/impl/BookTip.cpp src/ripple/app/tx/impl/CancelCheck.cpp @@ -576,6 +587,7 @@ target_sources (rippled PRIVATE src/ripple/rpc/handlers/DepositAuthorized.cpp src/ripple/rpc/handlers/DownloadShard.cpp src/ripple/rpc/handlers/Feature1.cpp + src/ripple/rpc/handlers/FederatorInfo.cpp src/ripple/rpc/handlers/Fee1.cpp src/ripple/rpc/handlers/FetchInfo.cpp src/ripple/rpc/handlers/GatewayBalances.cpp diff --git a/Builds/levelization/results/loops.txt b/Builds/levelization/results/loops.txt index d1838c55c1..8905de2619 100644 --- a/Builds/levelization/results/loops.txt +++ b/Builds/levelization/results/loops.txt @@ -11,7 +11,7 @@ Loop: ripple.app ripple.nodestore ripple.app > ripple.nodestore Loop: ripple.app ripple.overlay - ripple.overlay ~= ripple.app + ripple.overlay == ripple.app Loop: ripple.app ripple.peerfinder ripple.peerfinder ~= ripple.app diff --git a/Builds/levelization/results/ordering.txt b/Builds/levelization/results/ordering.txt index ed6b4e57c3..aba4e40ab0 100644 --- a/Builds/levelization/results/ordering.txt +++ b/Builds/levelization/results/ordering.txt @@ -6,6 +6,7 @@ ripple.app > ripple.crypto ripple.app > ripple.json ripple.app > ripple.protocol ripple.app > ripple.resource +ripple.app > ripple.server ripple.app > test.unit_test ripple.basics > ripple.beast ripple.conditions > ripple.basics diff --git a/CMakeLists.txt b/CMakeLists.txt index ade87d3498..b4b55d4d21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,12 @@ if(Git_FOUND) endif() endif() #git +if (thread_safety_analysis) + add_compile_options(-Wthread-safety -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS -DRIPPLE_ENABLE_THREAD_SAFETY_ANNOTATIONS) + add_compile_options("-stdlib=libc++") + add_link_options("-stdlib=libc++") +endif() + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/Builds/CMake") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/Builds/CMake/deps") diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 933c493911..1163a02eee 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -215,6 +216,8 @@ public: RCLValidations mValidations; std::unique_ptr m_loadManager; std::unique_ptr txQ_; + + std::shared_ptr sidechainFederator_; ClosureCounter waitHandlerCounter_; boost::asio::steady_timer sweepTimer_; boost::asio::steady_timer entropyTimer_; @@ -519,6 +522,8 @@ public: checkSigs(bool) override; bool isStopping() const override; + void + startFederator() override; int fdRequired() const override; @@ -889,6 +894,12 @@ public: return *mWalletDB; } + std::shared_ptr + getSidechainFederator() override + { + return sidechainFederator_; + } + ReportingETL& getReportingETL() override { @@ -1057,6 +1068,8 @@ public: ledgerCleaner_->stop(); if (reportingETL_) reportingETL_->stop(); + if (sidechainFederator_) + sidechainFederator_->stop(); if (auto pg = dynamic_cast( &*mRelationalDBInterface)) pg->stop(); @@ -1162,7 +1175,9 @@ public: getInboundLedgers().sweep(); getLedgerReplayer().sweep(); m_acceptedLedgerCache.sweep(); - cachedSLEs_.sweep(); + cachedSLEs_.expire(); + if (sidechainFederator_) + sidechainFederator_->sweep(); #ifdef RIPPLED_REPORTING if (auto pg = dynamic_cast( @@ -1597,6 +1612,15 @@ ApplicationImp::setup() if (reportingETL_) reportingETL_->start(); + sidechainFederator_ = sidechain::make_Federator( + *this, + get_io_service(), + *config_, + logs_->journal("SidechainFederator")); + + if (sidechainFederator_) + sidechainFederator_->start(); + return true; } @@ -1621,6 +1645,7 @@ ApplicationImp::start(bool withTimers) grpcServer_->start(); ledgerCleaner_->start(); perfLog_->start(); + startFederator(); } void @@ -1678,6 +1703,13 @@ ApplicationImp::isStopping() const return isTimeToStop; } +void +ApplicationImp::startFederator() +{ + if (sidechainFederator_) + sidechainFederator_->unlockMainLoop(); +} + int ApplicationImp::fdRequired() const { diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index 0fc927ff65..446da2befc 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -50,6 +50,10 @@ namespace RPC { class ShardArchiveHandler; } +namespace sidechain { +class Federator; +} + // VFALCO TODO Fix forward declares required for header dependency loops class AmendmentTable; @@ -150,6 +154,9 @@ public: virtual bool isStopping() const = 0; + virtual void + startFederator() = 0; + // // --- // @@ -274,6 +281,9 @@ public: virtual DatabaseCon& getWalletDB() = 0; + virtual std::shared_ptr + getSidechainFederator() = 0; + /** Ensure that a newly-started validator does not sign proposals older * than the last ledger it persisted. */ virtual LedgerIndex diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index 14befa63ea..ebe2e5f294 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ripple/app/sidechain/Federator.cpp b/src/ripple/app/sidechain/Federator.cpp new file mode 100644 index 0000000000..ddd6302e5d --- /dev/null +++ b/src/ripple/app/sidechain/Federator.cpp @@ -0,0 +1,2123 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace ripple { +namespace sidechain { + +[[nodiscard]] Federator::ChainType +srcChainType(event::Dir dir) +{ + return dir == event::Dir::mainToSide ? Federator::ChainType::mainChain + : Federator::ChainType::sideChain; +} + +[[nodiscard]] Federator::ChainType +dstChainType(event::Dir dir) +{ + return dir == event::Dir::mainToSide ? Federator::ChainType::sideChain + : Federator::ChainType::mainChain; +} + +[[nodiscard]] Federator::ChainType +otherChainType(Federator::ChainType ct) +{ + return ct == Federator::ChainType::mainChain + ? Federator::ChainType::sideChain + : Federator::ChainType::mainChain; +} + +[[nodiscard]] Federator::ChainType +getChainType(bool isMainchain) +{ + return isMainchain ? Federator::ChainType::mainChain + : Federator::ChainType::sideChain; +} + +[[nodiscard]] uint256 +crossChainTxnSignatureId( + PublicKey signingPK, + uint256 const& srcChainTxnHash, + std::optional const& dstChainTxnHash, + STAmount const& amt, + AccountID const& src, + AccountID const& dst, + std::uint32_t seq, + Slice const& signature) +{ + Serializer s(512); + s.addBitString(src); + s.addBitString(dst); + amt.add(s); + s.add32(seq); + s.addBitString(srcChainTxnHash); + if (dstChainTxnHash) + s.addBitString(*dstChainTxnHash); + s.addVL(signingPK.slice()); + s.addVL(signature); + + return s.getSHA512Half(); +} + +namespace detail { + +std::string const rootAccount{"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"}; + +// Return the txnType as a hex id for use in a transaction memo +char const* +memoHex(Federator::TxnType txnType) +{ + constexpr static char const* names[Federator::txnTypeLast] = {"0", "1"}; + return names[static_cast>( + txnType)]; +} + +Json::Value +getMemos( + Federator::TxnType txnType, + uint256 const& srcChainTxnHash, + // xChainTxnHash is used for refunds + std::optional const& dstChainTxnHash = std::nullopt) +{ + Json::Value memos{Json::arrayValue}; + { + Json::Value memo; + memo[jss::Memo][jss::MemoData] = memoHex(txnType); + memos.append(std::move(memo)); + } + { + Json::Value memo; + memo[jss::Memo][jss::MemoData] = to_string(srcChainTxnHash); + memos.append(std::move(memo)); + } + if (dstChainTxnHash) + { + Json::Value memo; + memo[jss::Memo][jss::MemoData] = to_string(*dstChainTxnHash); + memos.append(std::move(memo)); + } + return memos; +} + +[[nodiscard]] Json::Value +getTxn( + AccountID const& acc, + AccountID const& dst, + STAmount const& amt, + std::uint32_t seq, + Json::Value memos) +{ + Json::Value txnJson; + // TODO: determine fee + XRPAmount const fee{100}; + txnJson[jss::TransactionType] = "Payment"; + // TODO: Cache these strings instead of always converting to base 58 + txnJson[jss::Account] = toBase58(acc); + txnJson[jss::Destination] = toBase58(dst); + txnJson[jss::Amount] = amt.getJson(JsonOptions::none); + txnJson[jss::Sequence] = seq; + txnJson[jss::Fee] = to_string(fee); + txnJson[jss::Memos] = std::move(memos); + + return txnJson; +}; + +[[nodiscard]] STTx +getSignedTxn( + std::vector> const& sigs, + AccountID const& acc, + AccountID const& dst, + STAmount const& amt, + std::uint32_t seq, + Json::Value memos, + beast::Journal j) +{ + assert(sigs.size() > 1); + auto const txnJson = detail::getTxn(acc, dst, amt, seq, std::move(memos)); + + STParsedJSONObject parsed(std::string(jss::tx_json), txnJson); + if (parsed.object == std::nullopt) + { + JLOGV(j.fatal(), "invalid transaction", jv("tx", txnJson)); + assert(0); + } + try + { + parsed.object->setFieldVL(sfSigningPubKey, Slice(nullptr, 0)); + STTx txn(std::move(parsed.object.value())); + + STArray signers; + signers.reserve(sigs.size()); + for (auto const& [pk, sig] : sigs) + { + STObject obj{sfSigner}; + obj[sfAccount] = calcAccountID(pk); + obj[sfSigningPubKey] = pk; + obj[sfTxnSignature] = *sig; + signers.push_back(std::move(obj)); + }; + + std::sort( + signers.begin(), + signers.end(), + [](STObject const& lhs, STObject const& rhs) { + return lhs[sfAccount] < rhs[sfAccount]; + }); + + txn.setFieldArray(sfSigners, std::move(signers)); + return txn; + } + catch (...) + { + JLOGV(j.fatal(), "invalid transaction", jv("txn", txnJson)); + assert(0); + } +}; + +// Return the serialization of the txn with all the fields except the signing ID +// This will be used to verify signatures as they arrive. +[[nodiscard]] std::optional +getPartialSerializedTxn( + AccountID const& acc, + AccountID const& dst, + STAmount const& amt, + std::uint32_t seq, + Json::Value memos, + beast::Journal j) +{ + auto const txnJson = detail::getTxn(acc, dst, amt, seq, std::move(memos)); + + STParsedJSONObject parsed(std::string(jss::tx_json), txnJson); + if (parsed.object == std::nullopt) + { + JLOGV(j.fatal(), "invalid transaction", jv("tx", txnJson)); + assert(0); + } + try + { + parsed.object->setFieldVL(sfSigningPubKey, Slice(nullptr, 0)); + STTx txn(std::move(parsed.object.value())); + Serializer s; + s.add32(HashPrefix::txMultiSign); + txn.addWithoutSigningFields(s); + return s.getData(); + } + catch (...) + { + JLOGV(j.fatal(), "invalid transaction", jv("tx", txnJson)); + assert(0); + } + return {}; // should never happen +}; + +// For each line in a stanza whose lines all contain a single word (no words +// separated by spaces) call the function `callback` with the single word on +// each line with the leading and trailing spaces removed filter out the empty +// lines and comments. If the stanza line contains a multiple words, call the +// function `errorCallback` with the line and return. +template +void +foreachStanzaWord(Section const& stanza, F&& callback, EF&& errorCallback) +{ + std::vector elements; + elements.reserve(3); + for (auto const& l : stanza.lines()) + { + if (l.empty() || l[0] == '#') + continue; + boost::split(elements, l, boost::is_any_of("\t ")); + + { + // Consecutive spaces can leave empty strings. Remove them + auto const it = std::remove_if( + elements.begin(), elements.end(), [&](std::string const& e) { + return e.empty(); + }); + elements.erase(it, elements.end()); + } + + if (elements.size() != 1) + { + errorCallback(l); + return; + } + + callback(elements[0]); + elements.clear(); + } +} + +[[nodiscard]] hash_set +parseFederators(BasicConfig const& config, beast::Journal j) +{ + hash_set result; + + if (!config.exists("sidechain_federators")) + { + std::string const msg = "missing sidechain_federators stanza"; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + auto const& stanza = config["sidechain_federators"]; + + auto errorCallback = [&](std::string const& l) { + std::string const msg = "invalid sidechain_federators line: " + l; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + }; + + auto callback = [&](std::string const& element) { + auto pk = parseBase58(TokenType::AccountPublic, element); + if (!pk) + { + std::string const msg = + "invalid sidechain_federators public key: " + element; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + result.insert(*pk); + }; + + detail::foreachStanzaWord(stanza, callback, errorCallback); + + if (result.size() > STTx::maxMultiSigners || + result.size() < STTx::minMultiSigners) + { + std::ostringstream ostr; + ostr << "There must be at least " << STTx::minMultiSigners + << " and at most " << STTx::maxMultiSigners + << " federators. Num specified: " << result.size(); + JLOG(j.fatal()) << ostr.str(); + ripple::Throw(ostr.str()); + } + + return result; +} + +[[nodiscard]] std::vector> +parseFederatorSecrets(BasicConfig const& config, beast::Journal j) +{ + std::vector> result; + + if (!config.exists("sidechain_federators_secrets")) + { + std::string const msg = "Missing sidechain_federators_secrets stanza"; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + auto const& stanza = config["sidechain_federators_secrets"]; + + std::vector elements; + elements.reserve(3); + for (auto const& l : stanza.lines()) + { + if (l.empty() || l[0] == '#') + continue; + boost::split(elements, l, boost::is_any_of("\t ")); + + { + // Consecutive spaces can leave empty strings. Remove them + auto const it = std::remove_if( + elements.begin(), elements.end(), [&](std::string const& e) { + return e.empty(); + }); + elements.erase(it, elements.end()); + } + + if (elements.size() != 1) + { + std::string const msg = + "invalid sidechain_federators_secrets line: " + l; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + auto seed = parseBase58(elements[0]); + if (!seed) + { + std::string const msg = + "invalid sidechain_federators_secrets key: " + elements[0]; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + result.push_back(generateKeyPair(KeyType::ed25519, *seed)); + elements.clear(); + } + + return result; +} + +// set the value of `toSet` to the max of its current value and `reqValue` using +// a lock-free algorithm. +void +lockfreeSetMax(std::atomic& toSet, std::uint32_t reqValue) +{ + for (auto oldValue = toSet.load();;) + { + auto const newValue = std::max(oldValue, reqValue); + if (toSet.compare_exchange_strong(oldValue, newValue)) + break; + } +} + +} // namespace detail + +// Throws a logic error if the config is invalid +std::array< + boost::container::flat_map, + Federator::numChains> +Federator::makeAssetProps(BasicConfig const& config, beast::Journal j) +{ + // Make an STAmount from json string + auto makeSTAmount = [&](Section const& section, + std::string const& name, + beast::Journal j) -> STAmount { + auto const strOpt = section.get(name); + if (!strOpt) + { + std::string const msg = + "invalid sidechain assets stanza. Missing " + name; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + auto const dummyFieldName = "amount"; + std::string const jsonStr = [&] { + std::ostringstream ostr; + ostr << R"({")" << dummyFieldName << R"(":)" << *strOpt << '}'; + return ostr.str(); + }(); + + Json::Reader jr; + Json::Value jv; + if (!jr.parse(jsonStr, jv)) + { + std::string const msg = + "invalid sidechain assets stanza. Invalid amount " + *strOpt + + " for " + name; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + try + { + return amountFromJson(sfGeneric, jv[dummyFieldName]); + } + catch (...) + { + std::string const msg = + "invalid sidechain assets stanza. Invalid amount " + *strOpt + + " for " + name; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + }; + + // return the mainchain and sidechain "OtherChainAssetProperties" from the + // sidechain asset stanza. The assets specified the same way a payment + // amount is specified The ratio of the sidechain_asset amount to the + // mainchain_asset amount determines the mainchain + // "OtherChainAssetProperties" quality. The stanza should look like the + // following: + // mainchain_asset="1" + // mainchain_refund_penalty="200" + // sidechain_asset="2" + // sidechain_refund_penalty="200" + auto makeAssetPair = [&](Section const& section, beast::Journal j) + -> std::pair { + // TODO Don't hardcode the stanza key values + auto const mainchainAsset = makeSTAmount(section, "mainchain_asset", j); + auto const sidechainAsset = makeSTAmount(section, "sidechain_asset", j); + auto const mainchainRefundPenalty = + makeSTAmount(section, "mainchain_refund_penalty", j); + auto const sidechainRefundPenalty = + makeSTAmount(section, "sidechain_refund_penalty", j); + + for (auto const& a : + {mainchainAsset, + sidechainAsset, + mainchainRefundPenalty, + sidechainRefundPenalty}) + { + if (a.negative()) + { + std::string const msg = + "invalid sidechain assets stanza. All values must be " + "non-negative"; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + } + + if (mainchainAsset.issue() != mainchainRefundPenalty.issue()) + { + std::string const msg = + "invalid sidechain assets stanza. Mainchain asset and " + "mainchain refund penalty must have the same issue"; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + if (sidechainAsset.issue() != sidechainRefundPenalty.issue()) + { + std::string const msg = + "invalid sidechain assets stanza. Sidechain asset and " + "sidechain refund penalty must have the same issue"; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + if (mainchainAsset == mainchainAsset.zeroed()) + { + std::string const msg = + "invalid sidechain assets stanza. Mainchain asset must be a " + "positive amount"; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + if (sidechainAsset == sidechainAsset.zeroed()) + { + std::string const msg = + "invalid sidechain assets stanza. Sidechain asset must be a " + "positive amount"; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + OtherChainAssetProperties mainOCAP{ + Quality{sidechainAsset, mainchainAsset}, + sidechainAsset.issue(), + mainchainRefundPenalty}; + OtherChainAssetProperties sideOCAP{ + Quality{mainchainAsset, sidechainAsset}, + mainchainAsset.issue(), + sidechainRefundPenalty}; + + return {mainOCAP, sideOCAP}; + }; + + if (!config.exists("sidechain_assets")) + { + std::string const msg = "missing sidechain_assets stanza"; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + auto errorCallback = [&](std::string const& l) { + std::string const msg = "invalid sidechain_assets line: " + l; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + }; + + std::vector assetSectionNames; + assetSectionNames.reserve(3); + auto callback = [&](std::string const& element) { + assetSectionNames.push_back(element); + }; + + detail::foreachStanzaWord( + config["sidechain_assets"], callback, errorCallback); + + std::array< + boost::container::flat_map, + Federator::numChains> + result; + + for (auto const& n : assetSectionNames) + { + if (!config.exists(n)) + { + std::string const msg = "missing sidechain_asset stanza: " + n; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + auto [mainOCAP, sideOCAP] = makeAssetPair(config[n], j); + + if (result[mainChain].contains(sideOCAP.issue)) + { + std::string const msg = + "Duplicate mainchain_asset: " + to_string(sideOCAP.issue); + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + if (result[sideChain].contains(mainOCAP.issue)) + { + std::string const msg = + "Duplicate sidechain_asset: " + to_string(mainOCAP.issue); + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + result[mainChain][sideOCAP.issue] = mainOCAP; + result[sideChain][mainOCAP.issue] = sideOCAP; + } + + if (result[mainChain].empty()) + { + std::string const msg = "Must specify at least one sidechain asset"; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + return result; +} + +std::shared_ptr +make_Federator( + Application& app, + boost::asio::io_service& ios, + BasicConfig const& config, + beast::Journal j) +{ + if (!config.exists("sidechain")) + return {}; + auto const& sidechain = config["sidechain"]; + auto const keyStr = sidechain.get("signing_key"); + auto const ipStr = sidechain.get("mainchain_ip"); + auto const port = sidechain.get("mainchain_port_ws"); + auto const mainAccountStr = sidechain.get("mainchain_account"); + + if (!(keyStr && ipStr && port && mainAccountStr)) + { + auto const missing = [&]() -> std::string { + std::ostringstream ostr; + int numMissing = 0; + if (!keyStr) + { + ostr << "signing_key"; + ++numMissing; + } + if (!ipStr) + { + if (!numMissing) + ostr << "mainchain_ip"; + else + ostr << ", mainchain_ip"; + ++numMissing; + } + if (!port) + { + if (!numMissing) + ostr << "mainchain_port_ws"; + else + ostr << ", mainchain_port_ws"; + ++numMissing; + } + if (!mainAccountStr) + { + if (!numMissing) + ostr << "mainchain_account"; + else + ostr << ", mainchain_account"; + ++numMissing; + } + return ostr.str(); + }(); + std::string const msg = "invalid Sidechain stanza. Missing " + missing; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + return {}; + } + + auto key = parseBase58(TokenType::AccountSecret, *keyStr); + if (!key) + { + if (auto const seed = parseBase58(*keyStr)) + { + // TODO: we don't know the key type + key = generateKeyPair(KeyType::ed25519, *seed).second; + } + } + + if (!key) + { + std::string const msg = "invalid Sidechain signing key"; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + boost::asio::ip::address ip; + { + boost::system::error_code ec; + ip = boost::asio::ip::make_address(ipStr->c_str(), ec); + if (ec) + { + std::string const msg = + "invalid Sidechain ip address for the main chain: " + *ipStr; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + } + auto const mainAccount = parseBase58(*mainAccountStr); + if (!mainAccount) + { + std::string const msg = + "invalid Sidechain account for the main chain: " + *mainAccountStr; + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + hash_set federators = detail::parseFederators(config, j); + + if (!federators.count(derivePublicKey(KeyType::ed25519, *key))) + { + std::ostringstream ostr; + ostr << "Signing key is not part of the federator's set: " + << toBase58( + TokenType::AccountPublic, + derivePublicKey(KeyType::ed25519, *key)); + auto const msg = ostr.str(); + JLOG(j.fatal()) << msg; + ripple::Throw(msg); + } + + auto const sideAccount = parseBase58(detail::rootAccount); + assert(sideAccount); + + auto assetProps = Federator::makeAssetProps(config, j); + + auto r = std::make_shared( + Federator::PrivateTag{}, + app, + *key, + std::move(federators), + ip, + *port, + *mainAccount, + *sideAccount, + std::move(assetProps), + j); + + std::shared_ptr mainchainListener = + std::make_shared(*mainAccount, r, j); + std::shared_ptr sidechainListener = + std::make_shared( + app.getOPs(), *sideAccount, r, app, j); + r->init( + ios, + ip, + *port, + std::move(mainchainListener), + std::move(sidechainListener)); + + return r; +} + +Federator::Federator( + PrivateTag, + Application& app, + SecretKey signingKey, + hash_set&& federators, + boost::asio::ip::address mainChainIp, + std::uint16_t mainChainPort, + AccountID const& mainAccount, + AccountID const& sideAccount, + std::array< + boost::container::flat_map, + numChains>&& assetProps, + beast::Journal j) + : app_{app} + , account_{sideAccount, mainAccount} + , assetProps_{std::move(assetProps)} + /* TODO we don't know that they key type is ed25519 */ + , signingPK_{derivePublicKey(KeyType::ed25519, signingKey)} + , signingSK_{signingKey} + , federatorPKs_{std::move(federators)} + , mainSignerList_{mainAccount, federatorPKs_, app_.journal("mainFederatorSignerList")} + , sideSignerList_{sideAccount, federatorPKs_, app_.journal("sideFederatorSignerList")} + , mainSigCollector_{true, signingSK_, signingPK_, stopwatch(), mainSignerList_, *this, app, app_.journal("mainFederatorSigCollector")} + , sideSigCollector_{false, signingSK_, signingPK_, stopwatch(), sideSignerList_, *this, app, app_.journal("sideFederatorSigCollector")} + , ticketRunner_{mainAccount, sideAccount, *this, app_.journal("FederatorTicket")} + , mainDoorKeeper_{true, mainAccount, ticketRunner_, *this, app_.journal("mainFederatorDoorKeeper")} + , sideDoorKeeper_{false, sideAccount, ticketRunner_, *this, app_.journal("sideFederatorDoorKeeper")} + , j_(j) +{ + events_.reserve(16); +} + +void +Federator::init( + boost::asio::io_service& ios, + boost::asio::ip::address& ip, + std::uint16_t port, + std::shared_ptr&& mainchainListener, + std::shared_ptr&& sidechainListener) +{ + mainchainListener_ = std::move(mainchainListener); + mainchainListener_->init(ios, ip, port); + sidechainListener_ = std::move(sidechainListener); + sidechainListener_->init(app_.getOPs()); + + mainSigCollector_.setRpcChannel(mainchainListener_); + sideSigCollector_.setRpcChannel(sidechainListener_); + ticketRunner_.setRpcChannel(true, mainchainListener_); + ticketRunner_.setRpcChannel(false, sidechainListener_); + mainDoorKeeper_.setRpcChannel(mainchainListener_); + sideDoorKeeper_.setRpcChannel(sidechainListener_); +} + +Federator::~Federator() +{ + assert(!running_); +} + +void +Federator::start() +{ + if (running_) + return; + requestStop_ = false; + running_ = true; + + thread_ = std::thread([this]() { + beast::setCurrentThreadName("Federator"); + this->mainLoop(); + }); +} + +void +Federator::stop() +{ + if (running_) + { + requestStop_ = true; + { + std::lock_guard l(m_); + cv_.notify_one(); + } + + thread_.join(); + running_ = false; + } + mainchainListener_->shutdown(); +} + +void +Federator::push(FederatorEvent&& e) +{ + bool notify = false; + { + std::lock_guard l{eventsMutex_}; + notify = events_.empty(); + events_.push_back(std::move(e)); + } + if (notify) + { + std::lock_guard l(m_); + cv_.notify_one(); + } +} + +void +Federator::setLastTxnSeqSentMax(ChainType chaintype, std::uint32_t reqValue) +{ + detail::lockfreeSetMax(lastTxnSeqSent_[chaintype], reqValue); +} + +void +Federator::setLastTxnSeqConfirmedMax( + ChainType chaintype, + std::uint32_t reqValue) +{ + detail::lockfreeSetMax(lastTxnSeqConfirmed_[chaintype], reqValue); +} + +void +Federator::setAccountSeqMax(ChainType chaintype, std::uint32_t reqValue) +{ + detail::lockfreeSetMax(accountSeq_[chaintype], reqValue); +} + +[[nodiscard]] std::optional +Federator::toOtherChainAmount(ChainType srcChain, STAmount const& from) const +{ + if (!assetProps_[srcChain].contains(from.issue())) + return {}; + + auto const& assetProp = assetProps_[srcChain].at(from.issue()); + // The `Quality` class actually stores the value as a "rate", which is the + // inverse of quality. This means it's easier to divide by rate rather than + // multiply by quality. We could store inverse quality in the asset prop, + // but that would cause even worse confusion. + return divRound( + from, + assetProp.quality.rate(), + assetProp.issue, + /*roundUp*/ false); +} + +void +Federator::payTxn( + TxnType txnType, + ChainType dstChain, + STAmount const& amt, + AccountID const& srcChainSrcAccount, + AccountID const& dst, + uint256 const& srcChainTxnHash, + std::optional const& dstChainTxnHash) +{ + using namespace std::chrono_literals; + + // not const so it may be moved from + auto memos = detail::getMemos(txnType, srcChainTxnHash, dstChainTxnHash); + + JLOGV( + j_.trace(), + "payTxn", + jv("dstChain", + (dstChain == Federator::ChainType::mainChain ? "main" : "side")), + jv("account", dst), + jv("amt", amt), + jv("memos", memos)); + + if (amt.signum() <= 0) + { + JLOG(j_.error()) << "invalid transaction amount: " << amt; + return; + } + + auto const seq = accountSeq_[dstChain]++; + + auto job = [federator = shared_from_this(), + txnType, + dstChain, + srcChainSrcAccount, + thisChainSrcAccount = account_[dstChain], + dstAccount = dst, + amt, + srcChainTxnHash, + dstChainTxnHash, + memos = std::move(memos), + seq, + signingPK = signingPK_, + signingSK = signingSK_, + j = j_](Job&) mutable { + auto const txnJson = detail::getTxn( + thisChainSrcAccount, dstAccount, amt, seq, std::move(memos)); + + std::optional optSig = [&]() -> std::optional { + STParsedJSONObject parsed(std::string(jss::tx_json), txnJson); + if (parsed.object == std::nullopt) + { + JLOGV(j.fatal(), "invalid transaction", jv("tx", txnJson)); + assert(0); + } + try + { + parsed.object->setFieldVL(sfSigningPubKey, Slice(nullptr, 0)); + STTx txn(std::move(parsed.object.value())); + + return txn.getMultiSignature( + calcAccountID(signingPK), signingPK, signingSK); + } + catch (...) + { + JLOGV(j.fatal(), "invalid transaction", jv("tx", txnJson)); + assert(0); + } + return {}; // should never happen + }(); + + if (!optSig) + return; + + Buffer sig{std::move(*optSig)}; + + { + // forward the signature to all the peers + std::shared_ptr toSend = [&] { + protocol::TMFederatorXChainTxnSignature m; + + ::protocol::TMFederatorChainType ct = dstChain == sideChain + ? ::protocol::fct_SIDE + : ::protocol::fct_MAIN; + ::protocol::TMFederatorTxnType tt = [&txnType] { + static_assert(txnTypeLast == 2, "Add new case below"); + switch (txnType) + { + case TxnType::xChain: + return ::protocol::ftxnt_XCHAIN; + case TxnType::refund: + return protocol::ftxnt_REFUND; + } + assert(0); // case statement above is exhaustive + return protocol::ftxnt_XCHAIN; + }(); + m.set_txntype(tt); + m.set_dstchain(ct); + m.set_signingpk(signingPK.data(), signingPK.size()); + m.set_srcchaintxnhash( + srcChainTxnHash.data(), srcChainTxnHash.size()); + if (dstChainTxnHash) + m.set_dstchaintxnhash( + dstChainTxnHash->data(), dstChainTxnHash->size()); + { + Serializer s; + amt.add(s); + m.set_amount(s.data(), s.size()); + } + m.set_srcchainsrcaccount( + srcChainSrcAccount.data(), srcChainSrcAccount.size()); + m.set_dstchainsrcaccount( + thisChainSrcAccount.data(), thisChainSrcAccount.size()); + m.set_dstchaindstaccount(dstAccount.data(), dstAccount.size()); + m.set_seq(seq); + m.set_signature(sig.data(), sig.size()); + + return std::make_shared( + m, protocol::mtFederatorXChainTxnSignature); + }(); + + Overlay& overlay = federator->app_.overlay(); + HashRouter& hashRouter = federator->app_.getHashRouter(); + uint256 const suppression = crossChainTxnSignatureId( + signingPK, + srcChainTxnHash, + dstChainTxnHash, + amt, + thisChainSrcAccount, + dstAccount, + seq, + sig); + + if (auto const toSkip = hashRouter.shouldRelay(suppression)) + { + overlay.foreach([&](std::shared_ptr const& p) { + hashRouter.addSuppressionPeer(suppression, p->id()); + if (toSkip->count(p->id())) + { + JLOGV( + j.trace(), + "not sending signature to peer", + jv("id", p->id()), + jv("suppression", suppression)); + return; + } + JLOGV( + j.trace(), + "sending signature to peer", + jv("id", p->id()), + jv("suppression", suppression)); + p->send(toSend); + }); + } + } + + federator->addPendingTxnSig( + txnType, + dstChain, + signingPK, + srcChainTxnHash, + dstChainTxnHash, + amt, + srcChainSrcAccount, + dstAccount, + seq, + std::move(sig)); + + if (federator->app_.config().standalone()) + { + std::optional txnOpt = [&]() -> std::optional { + STParsedJSONObject parsed(std::string(jss::tx_json), txnJson); + if (parsed.object == std::nullopt) + { + JLOGV(j.fatal(), "invalid transaction", jv("tx", txnJson)); + assert(0); + } + try + { + parsed.object->setFieldVL( + sfSigningPubKey, Slice(nullptr, 0)); + STTx txn(std::move(parsed.object.value())); + return txn; + } + catch (...) + { + JLOGV(j.fatal(), "invalid transaction", jv("tx", txnJson)); + assert(0); + } + return {}; // should never happen + }(); + + if (!txnOpt) + return; + STTx txn{std::move(*txnOpt)}; + + auto getSig = [&](STTx const& txn, + PublicKey const& pk, + SecretKey const& sk) -> std::optional { + try + { + return txn.getMultiSignature(calcAccountID(pk), pk, sk); + } + catch (...) + { + assert(0); + } + return {}; // should never happen + }; + + static std::vector> keypairs = + detail::parseFederatorSecrets(federator->app_.config(), j); + + for (auto const& [pk, sk] : keypairs) + { + if (pk == federator->signingPK_) + continue; // don't sign for this federator again + + if (auto sig = getSig(txn, pk, sk)) + { + federator->addPendingTxnSig( + txnType, + dstChain, + pk, + srcChainTxnHash, + dstChainTxnHash, + amt, + srcChainSrcAccount, + dstAccount, + seq, + std::move(*sig)); + } + } + } + + federator->updateDoorKeeper(dstChain); + }; + + app_.getJobQueue().addJob(jtFEDERATORSIGNATURE, "federator signature", job); +} + +void +Federator::onEvent(event::XChainTransferDetected const& e) +{ + auto const srcChain = srcChainType(e.dir_); + std::optional toSendAmt = + toOtherChainAmount(srcChain, e.deliveredAmt_); + + if (!toSendAmt) + { + // not an issue used for xchain transfers + JLOGV( + j_.trace(), + "XChainTransferDetected ignored", + jv("dstChain", + (dstChainType(e.dir_) == Federator::ChainType::mainChain + ? "main" + : "side")), + jv("amt", e.deliveredAmt_), + jv("src", e.src_), + jv("dst", e.dst_)); + return; + } + payTxn( + TxnType::xChain, + dstChainType(e.dir_), + *toSendAmt, + e.src_, + e.dst_, + e.txnHash_, + std::nullopt); +} + +void +Federator::sendRefund( + ChainType chaintype, + STAmount const& amt, + AccountID const& dst, + uint256 const& xChainTxnHash, + uint256 const& triggeringResultTxnHash) +{ + JLOGV( + j_.trace(), + "sendRefund", + jv("amt", amt), + jv("dst", dst), + jv("chain", (chaintype == ChainType::mainChain ? "main" : "side")), + jv("xChainTxnHash", xChainTxnHash), + jv("triggeringResultTxnHash", triggeringResultTxnHash)); + + payTxn( + TxnType::refund, + chaintype, + amt, + // the src chain src account and the dst and the same when refunding + dst, + dst, + xChainTxnHash, + triggeringResultTxnHash); +} + +void +Federator::onEvent(event::XChainTransferResult const& e) +{ + JLOGV(j_.trace(), "Federator::onEvent", jv("event", e.toJson())); + + // srcChain and dstChain are the chains of the triggering transaction. + // I.e. A srcChain of main is a transfer result is a transaction that + // happens on the sidechain (the triggering transaction happended on the + // mainchain) + auto const srcChain = srcChainType(e.dir_); + auto const dstChain = dstChainType(e.dir_); + + onResult(dstChain, e.txnSeq_); + + if (e.ter_ != tesSUCCESS) + { + std::lock_guard pendingTxnsLock{pendingTxnsM_}; + if (auto i = pendingTxns_[dstChain].find(e.srcChainTxnHash_); + i != pendingTxns_[dstChain].end()) + { + auto const& pendingTxn = i->second; + // TODO: How are we tracking failed refunds? + if (isTecClaim(e.ter_)) + { + // The triggering transaction happened on the src chain. The + // result transaction happened on the dst chain. Convert the + // amount on the dst chain to an amount on the src chain. + std::optional sentAmt = + toOtherChainAmount(dstChain, pendingTxn.amount); + std::optional const penalty = + [&]() -> std::optional { + if (!sentAmt) + return {}; + try + { + return assetProps_[srcChain] + .at(sentAmt->issue()) + .refundPenalty; + } + catch (...) + { + } + return {}; + }(); + + if (!sentAmt || !penalty || + penalty->issue() != sentAmt->issue()) + { + assert(0); + JLOGV( + j_.trace(), + "Failed XChainTransferResult Refund", + jv("reason", + "Logic error: penalty not found or of wrong issue"), + jv("penalty", penalty.value_or(STAmount{})), + jv("event", e.toJson()), + jv("sentAmt", sentAmt.value_or(STAmount{}))); + return; + } + + if (*sentAmt <= *penalty) + { + JLOGV( + j_.trace(), + "Failed XChainTransferResult Refund", + jv("reason", "Refund amount is less than penalty"), + jv("penalty", *penalty), + jv("event", e.toJson()), + jv("sentAmt", *sentAmt)); + } + STAmount const amt{*sentAmt - *penalty}; + AccountID dst = pendingTxn.srcChainSrcAccount; + sendRefund(srcChain, amt, dst, e.srcChainTxnHash_, e.txnHash_); + } + } + else + { + JLOGV( + j_.trace(), + "Failed XChainTransferResult Refund", + jv("reason", "Could not find pending transaction"), + jv("event", e.toJson())); + } + } + + { + // remove the signature from the signature collection + std::lock_guard pendingTxnsLock{pendingTxnsM_}; + pendingTxns_[dstChain].erase(e.srcChainTxnHash_); + } + + updateDoorKeeper(dstChain); +} + +void +Federator::onEvent(event::RefundTransferResult const& e) +{ + JLOGV(j_.trace(), "RefundTransferResult", jv("event", e.toJson())); + + auto const srcChain = srcChainType(e.dir_); + onResult(srcChain, e.txnSeq_); + + if (e.ter_ != tesSUCCESS) + { + // There's not much that can be done if a refund fails. + JLOGV( + j_.fatal(), + "Failed RefundChainTransferResult", + jv("reason", "Failed transaction"), + jv("event", e.toJson())); + } + + // remove the signature from the signature collection + std::lock_guard pendingTxnsLock{pendingTxnsM_}; + pendingTxns_[srcChain].erase(e.dstChainTxnHash_); +} + +void +Federator::onEvent(event::HeartbeatTimer const& e) +{ + JLOG(j_.trace()) << "HeartbeatTimer"; +} + +void +Federator::updateDoorKeeper(ChainType dstChain) +{ + std::uint32_t txnsCount = [&]() { + std::lock_guard pendingTxnsLock{pendingTxnsM_}; + return pendingTxns_[dstChain].size(); + }(); + + auto sourceChain = dstChain == ChainType::sideChain ? ChainType::mainChain + : ChainType::sideChain; + switch (sourceChain) + { + case ChainType::mainChain: + mainDoorKeeper_.updateQueueLength(txnsCount); + break; + case ChainType::sideChain: + sideDoorKeeper_.updateQueueLength(txnsCount); + break; + } +} + +void +Federator::onResult(ChainType chainType, std::uint32_t resultTxSeq) +{ + setLastTxnSeqSentMax(chainType, resultTxSeq); + setLastTxnSeqConfirmedMax(chainType, resultTxSeq); + sendTxns(); +} + +bool +Federator::alreadySent(ChainType chaintype, std::uint32_t seq) const +{ + return seq < lastTxnSeqSent_[chaintype]; +} + +void +Federator::setLastXChainTxnWithResult( + ChainType chaintype, + std::uint32_t seq, + std::uint32_t seqTook, + uint256 const& hash) +{ + ChainType const otherChain = otherChainType(chaintype); + setLastTxnSeqSentMax(otherChain, seq); + setLastTxnSeqConfirmedMax(otherChain, seq); + accountSeq_[otherChain] = seq + seqTook; + + switch (chaintype) + { + case ChainType::mainChain: + mainchainListener_->setLastXChainTxnWithResult(hash); + break; + case ChainType::sideChain: + sidechainListener_->setLastXChainTxnWithResult(hash); + break; + } +} + +void +Federator::setNoLastXChainTxnWithResult(ChainType chaintype) +{ + switch (chaintype) + { + case ChainType::mainChain: + mainchainListener_->setNoLastXChainTxnWithResult(); + break; + case ChainType::sideChain: + sidechainListener_->setNoLastXChainTxnWithResult(); + break; + } +} + +void +Federator::stopHistoricalTxns(ChainType chaintype) +{ + switch (chaintype) + { + case ChainType::mainChain: + mainchainListener_->stopHistoricalTxns(); + break; + case ChainType::sideChain: + sidechainListener_->stopHistoricalTxns(app_.getOPs()); + break; + } +} + +void +Federator::initialSyncDone(ChainType chaintype) +{ + switch (chaintype) + { + case ChainType::mainChain: + ticketRunner_.init(true); + mainDoorKeeper_.init(); + break; + case ChainType::sideChain: + ticketRunner_.init(false); + sideDoorKeeper_.init(); + break; + } +} + +void +Federator::addPendingTxnSig( + TxnType txnType, + ChainType chaintype, + PublicKey const& federatorPK, + uint256 const& srcChainTxnHash, + std::optional const& dstChainTxnHash, + STAmount const& amt, + AccountID const& srcChainSrcAccount, + AccountID const& dstChainDstAccount, + std::uint32_t seq, + Buffer&& sig) +{ + std::lock_guard l{federatorPKsMutex_}; + + std::uint32_t sigThreshold = std::numeric_limits::max(); + { + sigThreshold = + static_cast(std::ceil(federatorPKs_.size() * 0.8)); + auto i = federatorPKs_.find(federatorPK); + if (i == federatorPKs_.end()) + { + // Unknown sending federator + JLOGV( + j_.debug(), + "unknown sending federator", + jv("public_key", strHex(federatorPK)), + jv("amt", amt), + jv("srcChainTxnHash", srcChainTxnHash)); + return; + } + } + + if (alreadySent(chaintype, seq)) + { + JLOGV( + j_.debug(), + "transaction already sent", + jv("public_key", strHex(federatorPK)), + jv("amt", amt), + jv("seq", seq), + jv("srcChainTxnHash", srcChainTxnHash)); + return; + } + + { + std::lock_guard pendingTxnsLock{pendingTxnsM_}; + + auto& txns = + pendingTxns_[chaintype][dstChainTxnHash.value_or(srcChainTxnHash)]; + + bool const isLocalFederator = (federatorPK == signingPK_); + if (isLocalFederator && + (amt != txns.amount || + dstChainDstAccount != txns.dstChainDstAccount || + srcChainSrcAccount != txns.srcChainSrcAccount)) + { + // another federator sent a transaction that disagrees with the + // local federator's txn. + txns.amount = amt; + txns.srcChainSrcAccount = srcChainSrcAccount; + txns.dstChainDstAccount = dstChainDstAccount; + txns.sigs.clear(); + txns.sequenceInfo.clear(); + } + + { + auto i = txns.sigs.find(federatorPK); + if (i != txns.sigs.end()) + { + // remove the old seq + assert(txns.sequenceInfo[i->second.seq].count > 0); + --txns.sequenceInfo[i->second.seq].count; + JLOGV( + j_.trace(), + "duplicate federator signature", + jv("federator", strHex(federatorPK)), + jv("amt", amt), + jv("srcChainTxnHash", srcChainTxnHash)); + if (txns.sequenceInfo[i->second.seq].count == 0) + { + // No federator is proposing this sequence number + // anymore + txns.sequenceInfo.erase(i->second.seq); + } + } + { + // Check that the signature is valid + std::optional partialSerializationOpt = + [&]() -> std::optional { + if (auto i = txns.sequenceInfo.find(seq); + i != txns.sequenceInfo.end()) + { + return i->second.partialTxnSerialization; + } + return detail::getPartialSerializedTxn( + account_[chaintype], + dstChainDstAccount, + amt, + seq, + detail::getMemos( + txnType, srcChainTxnHash, dstChainTxnHash), + j_); + }(); + + if (!partialSerializationOpt) + return; + + Blob partialSerialization{std::move(*partialSerializationOpt)}; + + Serializer s( + partialSerialization.data(), partialSerialization.size()); + s.addBitString(calcAccountID(federatorPK)); + + if (!verify( + federatorPK, + s.slice(), + sig, + /*fullyCanonical*/ true)) + { + JLOGV( + j_.error(), + "invalid federator signature", + jv("federator", strHex(federatorPK)), + jv("amt", amt), + jv("srcChainTxnHash", srcChainTxnHash)); + return; + } + else + { + JLOGV( + j_.trace(), + "valid federator signature", + jv("federator", strHex(federatorPK)), + jv("amt", amt), + jv("srcChainTxnHash", srcChainTxnHash)); + } + + if (auto i = txns.sequenceInfo.find(seq); + i == txns.sequenceInfo.end()) + { + // Store the partialSerialization so it doesn't need to + // be recomputed + txns.sequenceInfo[seq].partialTxnSerialization = + partialSerialization; + } + } + txns.sigs.insert_or_assign( + i, federatorPK, PeerTxnSignature{std::move(sig), seq}); + ++txns.sequenceInfo[seq].count; + } + + if (txns.sequenceInfo[seq].count < sigThreshold) + { + JLOGV( + j_.trace(), + "not enouth signatures to send", + jv("federator", strHex(federatorPK)), + jv("amt", amt), + jv("seq", seq), + jv("srcChainTxnHash", srcChainTxnHash), + jv("count", txns.sequenceInfo[seq].count)); + + return; + } + + if (txns.queuedToSend_) + { + JLOGV( + j_.trace(), + "transaction already queued to send", + jv("amt", amt), + jv("seq", seq), + jv("srcChainTxnHash", srcChainTxnHash)); + return; + } + + if (auto i = txns.sigs.find(signingPK_); + i != txns.sigs.end() && i->second.seq != seq) + { + // TODO: this federator's sequence number needs to be adjusted + } + + // There are enough signatures. Queue this transaction to send. + + auto const sigs = [&, sigThreshold = sigThreshold]() + -> std::vector> { + std::vector> r; + r.reserve(sigThreshold); + for (auto& [pk, sig] : txns.sigs) + { + if (sig.seq != seq) + { + // a federator sent a signature for a different sequence + continue; + } + r.emplace_back(pk, &sig.sig); + if (r.size() == sigThreshold) + break; + } + assert(r.size() == sigThreshold); + return r; + }(); + + // not const so it may be moved from + STTx txn = detail::getSignedTxn( + sigs, + account_[chaintype], + dstChainDstAccount, + amt, + seq, + detail::getMemos(txnType, srcChainTxnHash, dstChainTxnHash), + j_); + + { + std::lock_guard l{toSendTxnsM_}; + JLOGV( + j_.trace(), + "adding to toSendTxns", + jv("chain", (chaintype == sideChain ? "Side" : "Main")), + jv("amt", amt), + jv("seq", seq), + jv("srcChainTxnHash", srcChainTxnHash), + jv("count", txns.sequenceInfo[seq].count)); + toSendTxns_[chaintype].emplace(seq, std::move(txn)); + } + + txns.queuedToSend_ = true; + // close scope to release the lock before sending the transactions + } + + sendTxns(); +} + +void +Federator::addPendingTxnSig( + ChainType chaintype, + const PublicKey& publicKey, + const uint256& mId, + Buffer&& sig) +{ + switch (chaintype) + { + case mainChain: + mainSigCollector_.processSig(mId, publicKey, std::move(sig), {}); + break; + case sideChain: + sideSigCollector_.processSig(mId, publicKey, std::move(sig), {}); + break; + default: + assert(false); + } +} + +void +Federator::sendTxns() +{ + // Only one thread at a time should run sendTxns or transactions may be + // sent multiple times + std::lock_guard l{sendTxnsMutex_}; + + auto sendSidechainTxn = [this](STTx const& txn) { + Json::Value const request = [&] { + Json::Value r; + // TODO add failHard + r[jss::method] = "submit"; + r[jss::jsonrpc] = "2.0"; + r[jss::ripplerpc] = "2.0"; + r[jss::tx_blob] = strHex(txn.getSerializer().peekData()); + return r; + }(); + + auto const r = [&] { + Resource::Charge loadType = Resource::feeReferenceRPC; + Resource::Consumer c; + RPC::JsonContext context{ + {j_, + app_, + loadType, + app_.getOPs(), + app_.getLedgerMaster(), + c, + Role::ADMIN, + {}, + {}, + RPC::apiMaximumSupportedVersion}, + std::move(request)}; + + Json::Value jvResult; + // Make the transfer on the side chain + RPC::doCommand(context, jvResult); + return jvResult; + }(); + + JLOGV(j_.trace(), "main to side submit", jv("result", r)); + + if (!r.isMember(jss::engine_result_code) || + r[jss::engine_result_code].asInt() != 0) + { + if (r.isMember(jss::engine_result) && + (r[jss::engine_result] == "tefPAST_SEQ" || + // TODO got a reply: + // "engine_result":"tesSUCCESS", + // "engine_result_code":-190, + // "engine_result_message": + // "The transaction was applied. + // Only final in a validated ledger." + r[jss::engine_result] == "tesSUCCESS" || + r[jss::engine_result] == "terQUEUED" || + r[jss::engine_result] == "telCAN_NOT_QUEUE_FEE")) + { + // TODO: This is OK, but we still need to look for a + // confirmation in the txn stream + } + else + { + auto const msg = + "could not transfer from the sidechain door account"; + JLOGV(j_.fatal(), msg, jv("tx", request), jv("result", r)); + auto const ter = [&]() -> std::optional { + if (r.isMember(jss::engine_result_code)) + return TER::fromInt(r[jss::engine_result_code].asInt()); + return std::nullopt; + }(); + // tec codes will trigger a refund in + // onEvent(XChainTransferResult) + if (!ter || !isTecClaim(*ter)) + ripple::Throw(msg); + } + } + + if (app_.config().standalone()) + app_.getOPs().acceptLedger(); + }; + + auto sendMainchainTxn = [this](STTx const& txn) { + Json::Value const request = [&] { + Json::Value r; + // TODO add failHard + r[jss::tx_blob] = strHex(txn.getSerializer().peekData()); + return r; + }(); + + // TODO: Save the id and listen for errors + auto const id = mainchainListener_->send("submit", request); + JLOGV(j_.trace(), "mainchain submit message id", jv("id", id)); + }; + + for (auto chain : {sideChain, mainChain}) + { + auto const curAccSeq = accountSeq_[chain].load(); + auto maxToSend = [&]() -> std::uint32_t { + auto const lastSent = lastTxnSeqSent_[chain].load(); + auto const lastConfirmed = lastTxnSeqConfirmed_[chain].load(); + assert(lastSent >= lastConfirmed); + auto const onFly = lastSent - lastConfirmed; + JLOGV( + j_.trace(), + "sendTxns, compute maxToSend", + jv("chain", (chain == sideChain ? "Side" : "Main")), + jv("lastSent", lastSent), + jv("lastConfirmed", lastConfirmed), + jv("onFly", onFly)); + if (onFly >= 8) + return 0; + else + return 8 - onFly; + }(); + + for (int seq = lastTxnSeqSent_[chain] + 1; + seq < curAccSeq && maxToSend > 0; + ++seq, --maxToSend) + { + { + std::lock_guard l{toSendTxnsM_}; + if (auto i = toSkipSeq_[chain].find(seq); + i != toSkipSeq_[chain].end()) + { + lastTxnSeqSent_[chain] = seq; + toSkipSeq_[chain].erase(i); + JLOGV( + j_.trace(), + "sendTxns", + jv("chain", (chain == sideChain ? "Side" : "Main")), + jv("skipping", seq)); + continue; + } + } + + auto const txn = [&]() -> std::optional { + std::lock_guard l{toSendTxnsM_}; + auto i = toSendTxns_[chain].find(seq); + if (i == toSendTxns_[chain].end()) + { + JLOGV( + j_.trace(), + "sendTxns", + jv("chain", (chain == sideChain ? "Side" : "Main")), + jv("breaking_on_tx_seq", seq), + jv("lastTxSeqSent", lastTxnSeqSent_[chain])); + if (!toSendTxns_[chain].empty()) + { + JLOGV( + j_.trace(), + "sendTxns: next toSend", + jv("chain", (chain == sideChain ? "Side" : "Main")), + jv("seq", toSendTxns_[chain].begin()->first)); + } + else + { + JLOG(j_.trace()) << "sendTxns: toSendtxns is empty"; + } + // Even if there are more transactions in the + // collection, they can not be sent until transactions + // will smaller sequence numbers have been sent. + return {}; + } + return std::move(i->second); + }(); + if (!txn) + break; + + setLastTxnSeqSentMax(chain, seq); + if (chain == sideChain) + { + sendSidechainTxn(*txn); + } + else + { + sendMainchainTxn(*txn); + } + } + { + // Remove all the txns that have been sent (including those + // added to the collection after the seq has been sent). + std::lock_guard l{toSendTxnsM_}; + toSendTxns_[chain].erase( + toSendTxns_[chain].begin(), + std::find_if( + toSendTxns_[chain].begin(), + toSendTxns_[chain].end(), + [sent = lastTxnSeqSent_[chain].load()](auto const& item) + -> bool { return item.first > sent; })); + } + } +} + +void +Federator::unlockMainLoop() +{ + std::lock_guard l(m_); + mainLoopLocked_ = false; + mainLoopCv_.notify_one(); +} + +void +Federator::mainLoop() +{ + { + std::unique_lock l{mainLoopMutex_}; + mainLoopCv_.wait(l, [this] { return !mainLoopLocked_; }); + } + + std::vector localEvents; + localEvents.reserve(16); + while (!requestStop_) + { + { + std::lock_guard l{eventsMutex_}; + assert(localEvents.empty()); + localEvents.swap(events_); + } + if (localEvents.empty()) + { + using namespace std::chrono_literals; + // In rare cases, an event may be pushed and the condition + // variable signaled before the condition variable is waited on. + // To handle this, set a timeout on the wait. + std::unique_lock l{m_}; + // Allow for spurious wakeups. The alternative requires locking the + // eventsMutex_ + cv_.wait_for(l, 1s); + continue; + } + + for (auto const& event : localEvents) + std::visit([this](auto&& e) { this->onEvent(e); }, event); + localEvents.clear(); + } +} + +void +Federator::onEvent(event::StartOfHistoricTransactions const& e) +{ + assert(0); // StartOfHistoricTransactions is only used in initial sync +} + +void +Federator::onEvent(event::TicketCreateTrigger const& e) +{ + Federator::ChainType toChain = e.dir_ == event::Dir::mainToSide + ? Federator::sideChain + : Federator::mainChain; + auto seq = accountSeq_[toChain].fetch_add(2); + ticketRunner_.onEvent(seq, e); +} + +void +Federator::onEvent(const event::TicketCreateResult& e) +{ + auto const [fromChain, toChain] = e.dir_ == event::Dir::mainToSide + ? std::make_pair(sideChain, mainChain) + : std::make_pair(mainChain, sideChain); + + onResult(fromChain, e.txnSeq_); + + if (e.memoStr_.empty()) + { + ticketRunner_.onEvent(0, e); + } + else + { + auto seq = accountSeq_[toChain].fetch_add(1); + ticketRunner_.onEvent(seq, e); + } +} + +void +Federator::onEvent(event::DepositAuthResult const& e) +{ + auto const chainType = + (e.dir_ == event::Dir::mainToSide ? sideChain : mainChain); + + onResult(chainType, e.txnSeq_); + + switch (e.dir_) + { + case event::Dir::mainToSide: + sideDoorKeeper_.onEvent(e); + break; + case event::Dir::sideToMain: + mainDoorKeeper_.onEvent(e); + break; + } +} + +void +Federator::onEvent(event::BootstrapTicket const& e) +{ + setAccountSeqMax(getChainType(e.isMainchain_), e.txnSeq_ + 1); + setLastTxnSeqSentMax(getChainType(e.isMainchain_), e.txnSeq_); + setLastTxnSeqConfirmedMax(getChainType(e.isMainchain_), e.txnSeq_); + ticketRunner_.onEvent(e); +} + +void +Federator::onEvent(event::DisableMasterKeyResult const& e) +{ + setAccountSeqMax(getChainType(e.isMainchain_), e.txnSeq_ + 1); + setLastTxnSeqSentMax(getChainType(e.isMainchain_), e.txnSeq_); + setLastTxnSeqConfirmedMax(getChainType(e.isMainchain_), e.txnSeq_); +} + +// void +// Federator::onEvent(event::SignerListSetResult const& e) +//{ +// assert(0); +//} + +Json::Value +Federator::getInfo() const +{ + Json::Value ret{Json::objectValue}; + + auto populatePendingTransaction = + [](PendingTransaction const& txn) -> Json::Value { + Json::Value r{Json::objectValue}; + r[jss::amount] = txn.amount.getJson(JsonOptions::none); + r[jss::destination_account] = to_string(txn.dstChainDstAccount); + Json::Value sigs{Json::arrayValue}; + for (auto const& [pk, sig] : txn.sigs) + { + Json::Value s{Json::objectValue}; + s[jss::public_key] = toBase58(TokenType::AccountPublic, pk); + s[jss::seq] = sig.seq; + sigs.append(s); + } + r[jss::signatures] = sigs; + return r; + }; + + auto populateChain = [&](auto const& listener, + ChainType chaintype) -> Json::Value { + Json::Value r{Json::objectValue}; + Json::Value pending{Json::arrayValue}; + { + std::lock_guard l1{pendingTxnsM_}; + + for (auto const& [k, v] : pendingTxns_[chaintype]) + { + auto txn = populatePendingTransaction(v); + txn[jss::hash] = strHex(k); + pending.append(txn); + } + } + r[jss::pending_transactions] = pending; + r[jss::listener_info] = listener.getInfo(); + r[jss::sequence] = accountSeq_[chaintype].load(); + r[jss::last_transaction_sent_seq] = lastTxnSeqSent_[chaintype].load(); + if (chaintype == ChainType::mainChain) + { + r["door_status"] = mainDoorKeeper_.getInfo(); + r["tickets"] = ticketRunner_.getInfo(true); + } + else + { + r["door_status"] = sideDoorKeeper_.getInfo(); + r["tickets"] = ticketRunner_.getInfo(false); + } + return r; + }; + + ret[jss::public_key] = toBase58(TokenType::AccountPublic, signingPK_); + ret[jss::mainchain] = + populateChain(*mainchainListener_, ChainType::mainChain); + ret[jss::sidechain] = + populateChain(*sidechainListener_, ChainType::sideChain); + + return ret; +} + +void +Federator::sweep() +{ + updateDoorKeeper(ChainType::mainChain); + updateDoorKeeper(ChainType::sideChain); + mainSigCollector_.expire(); + sideSigCollector_.expire(); +} + +SignatureCollector& +Federator::getSignatureCollector(ChainType chain) +{ + if (chain == ChainType::mainChain) + return mainSigCollector_; + else + return sideSigCollector_; +} + +TicketRunner& +Federator::getTicketRunner() +{ + return ticketRunner_; +} + +DoorKeeper& +Federator::getDoorKeeper(ChainType chain) +{ + if (chain == ChainType::mainChain) + return mainDoorKeeper_; + else + return sideDoorKeeper_; +} + +void +Federator::addSeqToSkip(ChainType chain, std::uint32_t seq) +{ + { + JLOGV( + j_.trace(), + "addSeqToSkip, ticket seq to skip when processing toSendTxns", + jv("chain", (chain == sideChain ? "Side" : "Main")), + jv("ticket seq", seq), + jv("account seq", accountSeq_[chain]), + jv("lastSent", lastTxnSeqSent_[chain])); + std::lock_guard l{toSendTxnsM_}; + toSkipSeq_[chain].insert(seq); + } + sendTxns(); +} + +void +Federator::addTxToSend(ChainType chain, std::uint32_t seq, STTx const& tx) +{ + { + std::lock_guard l{toSendTxnsM_}; + JLOGV( + j_.trace(), + "adding account control tx to toSendTxns", + jv("chain", (chain == sideChain ? "Side" : "Main")), + jv("seq", seq), + jv("account seq", accountSeq_[chain]), + jv("lastSent", lastTxnSeqSent_[chain])); + toSendTxns_[chain].emplace(seq, tx); + } + sendTxns(); +} +} // namespace sidechain +} // namespace ripple diff --git a/src/ripple/app/sidechain/Federator.h b/src/ripple/app/sidechain/Federator.h new file mode 100644 index 0000000000..4e368dcfe5 --- /dev/null +++ b/src/ripple/app/sidechain/Federator.h @@ -0,0 +1,418 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_FEDERATOR_H_INCLUDED +#define RIPPLE_SIDECHAIN_FEDERATOR_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +class Application; +class STTx; + +namespace sidechain { + +class Federator : public std::enable_shared_from_this +{ +public: + enum ChainType { sideChain, mainChain }; + + // These enums are encoded in the transaction. Changing the order will break + // backward compatibility. If a new type is added change txnTypeLast. + enum class TxnType { xChain, refund }; + constexpr static std::uint8_t txnTypeLast = 2; + + constexpr static size_t numChains = 2; + + static constexpr std::uint32_t accountControlTxFee{1000}; + +private: + // Tag so make_Federator can call `std::make_shared` + class PrivateTag + { + }; + + friend std::shared_ptr + make_Federator( + Application& app, + boost::asio::io_service& ios, + BasicConfig const& config, + beast::Journal j); + + std::thread thread_; + bool running_ = false; + std::atomic requestStop_ = false; + + Application& app_; + std::array const account_; + std::array, numChains> accountSeq_{1, 1}; + std::array, numChains> lastTxnSeqSent_{0, 0}; + std::array, numChains> lastTxnSeqConfirmed_{ + 0, + 0}; + std::shared_ptr mainchainListener_; + std::shared_ptr sidechainListener_; + + mutable std::mutex eventsMutex_; + std::vector GUARDED_BY(eventsMutex_) events_; + + // When a user account sends an asset to the account controlled by the + // federator, the asset to be issued on the other chain is determined by the + // `assetProps` maps - one for each chain. The asset to be issued is + // `issue`, the amount of the asset to issue is determined by `quality` + // (ratio of output amount/input amount). When issuing refunds, the + // `refundPenalty` is subtracted from the sent amount before sending the + // refund. + struct OtherChainAssetProperties + { + Quality quality; + Issue issue; + STAmount refundPenalty; + }; + + std::array< + boost::container::flat_map, + numChains> const assetProps_; + + PublicKey signingPK_; + SecretKey signingSK_; + + // federator signing public keys + mutable std::mutex federatorPKsMutex_; + hash_set GUARDED_BY(federatorPKsMutex_) federatorPKs_; + + SignerList mainSignerList_; + SignerList sideSignerList_; + SignatureCollector mainSigCollector_; + SignatureCollector sideSigCollector_; + TicketRunner ticketRunner_; + DoorKeeper mainDoorKeeper_; + DoorKeeper sideDoorKeeper_; + + struct PeerTxnSignature + { + Buffer sig; + std::uint32_t seq; + }; + + struct SequenceInfo + { + // Number of signatures at this sequence number + std::uint32_t count{0}; + // Serialization of the transaction for everything except the signature + // id (which varies for each signature). This can be used to verify one + // of the signatures in a multisig. + Blob partialTxnSerialization; + }; + + struct PendingTransaction + { + STAmount amount; + AccountID srcChainSrcAccount; + AccountID dstChainDstAccount; + // Key is the federator's public key + hash_map sigs; + // Key is a sequence number + hash_map sequenceInfo; + // True if the transaction was ever put into the toSendTxns_ queue + bool queuedToSend_{false}; + }; + + // Key is the hash of the triggering transaction + mutable std::mutex pendingTxnsM_; + hash_map GUARDED_BY(pendingTxnsM_) + pendingTxns_[numChains]; + + mutable std::mutex toSendTxnsM_; + // Signed transactions ready to send + // Key is the transaction's sequence number. The transactions must be sent + // in the correct order. If the next trasnaction the account needs to send + // has a sequence number of N, the transaction with sequence N+1 can't be + // sent just because it collected signatures first. + std::map GUARDED_BY(toSendTxnsM_) + toSendTxns_[numChains]; + std::set GUARDED_BY(toSendTxnsM_) toSkipSeq_[numChains]; + + // Use a condition variable to prevent busy waiting when the queue is + // empty + mutable std::mutex m_; + std::condition_variable cv_; + + // prevent the main loop from starting until explictly told to run. + // This is used to allow bootstrap code to run before any events are + // processed + mutable std::mutex mainLoopMutex_; + bool mainLoopLocked_{true}; + std::condition_variable mainLoopCv_; + beast::Journal j_; + + static std::array< + boost::container::flat_map, + numChains> + makeAssetProps(BasicConfig const& config, beast::Journal j); + +public: + // Constructor should be private, but needs to be public so + // `make_shared` can use it + Federator( + PrivateTag, + Application& app, + SecretKey signingKey, + hash_set&& federators, + boost::asio::ip::address mainChainIp, + std::uint16_t mainChainPort, + AccountID const& mainAccount, + AccountID const& sideAccount, + std::array< + boost::container::flat_map, + numChains>&& assetProps, + beast::Journal j); + + ~Federator(); + + void + start(); + + void + stop() EXCLUDES(m_); + + void + push(FederatorEvent&& e) EXCLUDES(m_, eventsMutex_); + + // Don't process any events until the bootstrap has a chance to run + void + unlockMainLoop() EXCLUDES(m_); + + void + addPendingTxnSig( + TxnType txnType, + ChainType chaintype, + PublicKey const& federatorPk, + uint256 const& srcChainTxnHash, + std::optional const& dstChainTxnHash, + STAmount const& amt, + AccountID const& srcChainSrcAccount, + AccountID const& dstChainDstAccount, + std::uint32_t seq, + Buffer&& sig) EXCLUDES(federatorPKsMutex_, pendingTxnsM_, toSendTxnsM_); + + void + addPendingTxnSig( + ChainType chaintype, + PublicKey const& publicKey, + uint256 const& mId, + Buffer&& sig); + + // Return true if a transaction with this sequence has already been sent + bool + alreadySent(ChainType chaintype, std::uint32_t seq) const; + + void + setLastXChainTxnWithResult( + ChainType chaintype, + std::uint32_t seq, + std::uint32_t seqTook, + uint256 const& hash); + void + setNoLastXChainTxnWithResult(ChainType chaintype); + void + stopHistoricalTxns(ChainType chaintype); + void + initialSyncDone(ChainType chaintype); + + // Get stats on the federator, including pending transactions and + // initialization state + Json::Value + getInfo() const EXCLUDES(pendingTxnsM_); + + void + sweep(); + + SignatureCollector& + getSignatureCollector(ChainType chain); + DoorKeeper& + getDoorKeeper(ChainType chain); + TicketRunner& + getTicketRunner(); + void + addSeqToSkip(ChainType chain, std::uint32_t seq) EXCLUDES(toSendTxnsM_); + // TODO multi-sig refactor? + void + addTxToSend(ChainType chain, std::uint32_t seq, STTx const& tx) + EXCLUDES(toSendTxnsM_); + +private: + // Two phase init needed for shared_from this. + // Only called from `make_Federator` + void + init( + boost::asio::io_service& ios, + boost::asio::ip::address& ip, + std::uint16_t port, + std::shared_ptr&& mainchainListener, + std::shared_ptr&& sidechainListener); + + // Convert between the asset on the src chain to the asset on the other + // chain. The `assetProps_` array controls how this conversion is done. + // An empty option is returned if the from issue is not part of the map in + // the `assetProps_` array. + [[nodiscard]] std::optional + toOtherChainAmount(ChainType srcChain, STAmount const& from) const; + + // Set the accountSeq to the max of the current value and the requested + // value. This is done with a lock free algorithm. + void + setAccountSeqMax(ChainType chaintype, std::uint32_t reqValue); + + // Set the lastTxnSeqSent to the max of the current value and the requested + // value. This is done with a lock free algorithm. + void + setLastTxnSeqSentMax(ChainType chaintype, std::uint32_t reqValue); + void + setLastTxnSeqConfirmedMax(ChainType chaintype, std::uint32_t reqValue); + + mutable std::mutex sendTxnsMutex_; + void + sendTxns() EXCLUDES(sendTxnsMutex_, toSendTxnsM_); + + void + mainLoop() EXCLUDES(mainLoopMutex_); + + void + payTxn( + TxnType txnType, + ChainType dstChain, + STAmount const& amt, + // srcChainSrcAccount is the origional sending account in a cross chain + // transaction. Note, for refunds, the srcChainSrcAccount and the dst + // will be the same. + AccountID const& srcChainSrcAccount, + AccountID const& dst, + uint256 const& srcChainTxnHash, + std::optional const& dstChainTxnHash); + + // Issue a refund to the destination account. Refunds may be issued when a + // cross chain transaction fails on the destination chain. In this case, the + // funds will already be locked on one chain, but can not be completed on + // the other chain. Note that refunds may not be for the full amount sent. + // In effect, not refunding the full amount charges a fee to discourage + // abusing refunds to try to overload the system. + void + sendRefund( + ChainType chaintype, + STAmount const& amt, + AccountID const& dst, + uint256 const& txnHash, + uint256 const& triggeringResultTxnHash); + + void + onEvent(event::XChainTransferDetected const& e); + void + onEvent(event::XChainTransferResult const& e) EXCLUDES(pendingTxnsM_); + void + onEvent(event::RefundTransferResult const& e) EXCLUDES(pendingTxnsM_); + void + onEvent(event::HeartbeatTimer const& e); + void + onEvent(event::StartOfHistoricTransactions const& e); + void + onEvent(event::TicketCreateTrigger const& e); + void + onEvent(event::TicketCreateResult const& e); + void + onEvent(event::DepositAuthResult const& e); + void + onEvent(event::BootstrapTicket const& e); + void + onEvent(event::DisableMasterKeyResult const& e); + // void + // onEvent(event::SignerListSetResult const& e); + + void + updateDoorKeeper(ChainType chainType) EXCLUDES(pendingTxnsM_); + void + onResult(ChainType chainType, std::uint32_t resultTxSeq); +}; + +[[nodiscard]] std::shared_ptr +make_Federator( + Application& app, + boost::asio::io_service& ios, + BasicConfig const& config, + beast::Journal j); + +// Id used for message suppression +[[nodiscard]] uint256 +crossChainTxnSignatureId( + PublicKey signingPK, + uint256 const& srcChainTxnHash, + std::optional const& dstChainTxnHash, + STAmount const& amt, + AccountID const& src, + AccountID const& dst, + std::uint32_t seq, + Slice const& signature); + +[[nodiscard]] Federator::ChainType +srcChainType(event::Dir dir); + +[[nodiscard]] Federator::ChainType +dstChainType(event::Dir dir); + +[[nodiscard]] Federator::ChainType +otherChainType(Federator::ChainType ct); + +[[nodiscard]] Federator::ChainType +getChainType(bool isMainchain); + +uint256 +computeMessageSuppression(uint256 const& mId, Slice const& signature); +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/app/sidechain/FederatorEvents.cpp b/src/ripple/app/sidechain/FederatorEvents.cpp new file mode 100644 index 0000000000..e9585ff9ce --- /dev/null +++ b/src/ripple/app/sidechain/FederatorEvents.cpp @@ -0,0 +1,340 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 + +namespace ripple { +namespace sidechain { +namespace event { + +namespace { + +std::string const& +to_string(Dir dir) +{ + switch (dir) + { + case Dir::mainToSide: { + static std::string const r("main"); + return r; + } + case Dir::sideToMain: { + static std::string const r("side"); + return r; + } + } + + // Some compilers will warn about not returning from all control paths + // without this, but this code will never execute. + assert(0); + static std::string const error("error"); + return error; +} + +std::string const& +to_string(AccountFlagOp op) +{ + switch (op) + { + case AccountFlagOp::set: { + static std::string const r("set"); + return r; + } + case AccountFlagOp::clear: { + static std::string const r("clear"); + return r; + } + } + + // Some compilers will warn about not returning from all control paths + // without this, but this code will never execute. + assert(0); + static std::string const error("error"); + return error; +} + +} // namespace + +EventType +XChainTransferDetected::eventType() const +{ + return EventType::trigger; +} + +Json::Value +XChainTransferDetected::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "XChainTransferDetected"; + result["src"] = toBase58(src_); + result["dst"] = toBase58(dst_); + result["deliveredAmt"] = deliveredAmt_.getJson(JsonOptions::none); + result["txnSeq"] = txnSeq_; + result["txnHash"] = to_string(txnHash_); + result["rpcOrder"] = rpcOrder_; + return result; +} + +EventType +HeartbeatTimer::eventType() const +{ + return EventType::heartbeat; +} + +Json::Value +HeartbeatTimer::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "HeartbeatTimer"; + return result; +} + +EventType +XChainTransferResult::eventType() const +{ + return EventType::result; +} + +Json::Value +XChainTransferResult::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "XChainTransferResult"; + result["dir"] = to_string(dir_); + result["dst"] = toBase58(dst_); + if (deliveredAmt_) + result["deliveredAmt"] = deliveredAmt_->getJson(JsonOptions::none); + result["txnSeq"] = txnSeq_; + result["srcChainTxnHash"] = to_string(srcChainTxnHash_); + result["txnHash"] = to_string(txnHash_); + result["ter"] = transHuman(ter_); + result["rpcOrder"] = rpcOrder_; + return result; +} + +EventType +RefundTransferResult::eventType() const +{ + return EventType::result; +} + +Json::Value +RefundTransferResult::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "RefundTransferResult"; + result["dir"] = to_string(dir_); + result["dst"] = toBase58(dst_); + if (deliveredAmt_) + result["deliveredAmt"] = deliveredAmt_->getJson(JsonOptions::none); + result["txnSeq"] = txnSeq_; + result["srcChainTxnHash"] = to_string(srcChainTxnHash_); + result["dstChainTxnHash"] = to_string(dstChainTxnHash_); + result["txnHash"] = to_string(txnHash_); + result["ter"] = transHuman(ter_); + result["rpcOrder"] = rpcOrder_; + return result; +} + +EventType +StartOfHistoricTransactions::eventType() const +{ + return EventType::startOfTransactions; +} + +Json::Value +StartOfHistoricTransactions::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "StartOfHistoricTransactions"; + result["isMainchain"] = isMainchain_; + return result; +} + +EventType +TicketCreateTrigger::eventType() const +{ + return EventType::trigger; +} + +Json::Value +TicketCreateTrigger::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "TicketCreateTrigger"; + result["dir"] = to_string(dir_); + result["success"] = success_; + result["txnSeq"] = txnSeq_; + result["ledgerIndex"] = ledgerIndex_; + result["txnHash"] = to_string(txnHash_); + result["rpcOrder"] = rpcOrder_; + result["sourceTag"] = sourceTag_; + result["memo"] = memoStr_; + return result; +} + +EventType +TicketCreateResult::eventType() const +{ + return EventType::resultAndTrigger; +} + +Json::Value +TicketCreateResult::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "TicketCreateResult"; + result["dir"] = to_string(dir_); + result["success"] = success_; + result["txnSeq"] = txnSeq_; + result["ledgerIndex"] = ledgerIndex_; + result["srcChainTxnHash"] = to_string(srcChainTxnHash_); + result["txnHash"] = to_string(txnHash_); + result["rpcOrder"] = rpcOrder_; + result["sourceTag"] = sourceTag_; + result["memo"] = memoStr_; + return result; +} + +void +TicketCreateResult::removeTrigger() +{ + memoStr_.clear(); +} + +EventType +DepositAuthResult::eventType() const +{ + return EventType::result; +} + +Json::Value +DepositAuthResult::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "DepositAuthResult"; + result["dir"] = to_string(dir_); + result["success"] = success_; + result["txnSeq"] = txnSeq_; + result["ledgerIndex"] = ledgerIndex_; + result["srcChainTxnHash"] = to_string(srcChainTxnHash_); + result["rpcOrder"] = rpcOrder_; + result["op"] = to_string(op_); + return result; +} + +EventType +SignerListSetResult::eventType() const +{ + return EventType::result; +} + +Json::Value +SignerListSetResult::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "SignerListSetResult"; + return result; +} +EventType +BootstrapTicket::eventType() const +{ + return EventType::bootstrap; +} + +Json::Value +BootstrapTicket::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "BootstrapTicket"; + result["isMainchain"] = isMainchain_; + result["txnSeq"] = txnSeq_; + result["rpcOrder"] = rpcOrder_; + return result; +} +EventType +DisableMasterKeyResult::eventType() const +{ + return EventType::result; // TODO change to bootstrap type too? +} + +Json::Value +DisableMasterKeyResult::toJson() const +{ + Json::Value result{Json::objectValue}; + result["eventType"] = "DisableMasterKeyResult"; + result["isMainchain"] = isMainchain_; + result["txnSeq"] = txnSeq_; + result["rpcOrder"] = rpcOrder_; + return result; +} + +} // namespace event + +namespace { +template +struct hasTxnHash : std::false_type +{ +}; +template +struct hasTxnHash().txnHash_)>> + : std::true_type +{ +}; +template +inline constexpr bool hasTxnHash_v = hasTxnHash::value; + +// Check that the traits work as expected +static_assert( + hasTxnHash_v && + !hasTxnHash_v, + ""); +} // namespace + +std::optional +txnHash(FederatorEvent const& event) +{ + return std::visit( + [](auto const& e) -> std::optional { + if constexpr (hasTxnHash_v>) + { + return e.txnHash_; + } + return std::nullopt; + }, + event); +} + +event::EventType +eventType(FederatorEvent const& event) +{ + return std::visit([](auto const& e) { return e.eventType(); }, event); +} + +Json::Value +toJson(FederatorEvent const& event) +{ + return std::visit([](auto const& e) { return e.toJson(); }, event); +} + +} // namespace sidechain +} // namespace ripple diff --git a/src/ripple/app/sidechain/FederatorEvents.h b/src/ripple/app/sidechain/FederatorEvents.h new file mode 100644 index 0000000000..2dc1821b30 --- /dev/null +++ b/src/ripple/app/sidechain/FederatorEvents.h @@ -0,0 +1,277 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_FEDERATOR_EVENTS_H_INCLUDED +#define RIPPLE_SIDECHAIN_FEDERATOR_EVENTS_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace ripple { +namespace sidechain { +namespace event { + +enum class Dir { sideToMain, mainToSide }; +enum class AccountFlagOp { set, clear }; +static constexpr std::uint32_t MemoStringMax = 512; + +enum class EventType { + bootstrap, + trigger, + result, + resultAndTrigger, + heartbeat, + startOfTransactions +}; + +// A cross chain transfer was detected on this federator +struct XChainTransferDetected +{ + // direction of the transfer + Dir dir_; + // Src account on the src chain + AccountID src_; + // Dst account on the dst chain + AccountID dst_; + STAmount deliveredAmt_; + std::uint32_t txnSeq_; + uint256 txnHash_; + std::int32_t rpcOrder_; + + EventType + eventType() const; + + Json::Value + toJson() const; +}; + +struct HeartbeatTimer +{ + EventType + eventType() const; + + Json::Value + toJson() const; +}; + +struct XChainTransferResult +{ + // direction is the direction of the triggering transaction. + // I.e. A "mainToSide" transfer result is a transaction that + // happens on the sidechain (the triggering transaction happended on the + // mainchain) + Dir dir_; + AccountID dst_; + std::optional deliveredAmt_; + std::uint32_t txnSeq_; + // Txn hash of the initiating xchain transaction + uint256 srcChainTxnHash_; + // Txn has of the federator's transaction on the dst chain + uint256 txnHash_; + TER ter_; + std::int32_t rpcOrder_; + + EventType + eventType() const; + + Json::Value + toJson() const; +}; + +struct RefundTransferResult +{ + // direction is the direction of the triggering transaction. + // I.e. A "mainToSide" refund transfer result is a transaction that + // happens on the mainchain (the triggering transaction happended on the + // mainchain, the failed result happened on the side chain, and the refund + // result happened on the mainchain) + Dir dir_; + AccountID dst_; + std::optional deliveredAmt_; + std::uint32_t txnSeq_; + // Txn hash of the initiating xchain transaction + uint256 srcChainTxnHash_; + // Txn hash of the federator's transaction on the dst chain + uint256 dstChainTxnHash_; + // Txn hash of the refund result + uint256 txnHash_; + TER ter_; + std::int32_t rpcOrder_; + + EventType + eventType() const; + + Json::Value + toJson() const; +}; + +// The start of historic transactions has been reached +struct StartOfHistoricTransactions +{ + bool isMainchain_; + + EventType + eventType() const; + + Json::Value + toJson() const; +}; + +struct TicketCreateTrigger +{ + Dir dir_; + bool success_; + std::uint32_t txnSeq_; + std::uint32_t ledgerIndex_; + uint256 txnHash_; + std::int32_t rpcOrder_; + + std::uint32_t sourceTag_; + std::string memoStr_; + + EventType + eventType() const; + + Json::Value + toJson() const; +}; + +struct TicketCreateResult +{ + Dir dir_; + bool success_; + std::uint32_t txnSeq_; + std::uint32_t ledgerIndex_; + uint256 srcChainTxnHash_; + uint256 txnHash_; + std::int32_t rpcOrder_; + + std::uint32_t sourceTag_; + std::string memoStr_; + + EventType + eventType() const; + + Json::Value + toJson() const; + + void + removeTrigger(); +}; + +struct DepositAuthResult +{ + Dir dir_; + bool success_; + std::uint32_t txnSeq_; + std::uint32_t ledgerIndex_; + uint256 srcChainTxnHash_; + std::int32_t rpcOrder_; + + AccountFlagOp op_; + + EventType + eventType() const; + + Json::Value + toJson() const; +}; + +struct SignerListSetResult +{ + // TODO + + EventType + eventType() const; + + Json::Value + toJson() const; +}; + +struct BootstrapTicket +{ + bool isMainchain_; + bool success_; + std::uint32_t txnSeq_; + std::uint32_t ledgerIndex_; + std::int32_t rpcOrder_; + + std::uint32_t sourceTag_; + + EventType + eventType() const; + + Json::Value + toJson() const; +}; + +struct DisableMasterKeyResult +{ + bool isMainchain_; + std::uint32_t txnSeq_; + std::int32_t rpcOrder_; + + EventType + eventType() const; + + Json::Value + toJson() const; +}; + +} // namespace event + +using FederatorEvent = std::variant< + event::XChainTransferDetected, + event::HeartbeatTimer, + event::XChainTransferResult, + event::RefundTransferResult, + event::StartOfHistoricTransactions, + event::TicketCreateTrigger, + event::TicketCreateResult, + event::DepositAuthResult, + event::BootstrapTicket, + event::DisableMasterKeyResult>; + +event::EventType +eventType(FederatorEvent const& event); + +Json::Value +toJson(FederatorEvent const& event); + +// If the event has a txnHash_ field (all the trigger events), return the hash, +// otherwise return nullopt +std::optional +txnHash(FederatorEvent const& event); + +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/app/sidechain/impl/ChainListener.cpp b/src/ripple/app/sidechain/impl/ChainListener.cpp new file mode 100644 index 0000000000..9f37aeae16 --- /dev/null +++ b/src/ripple/app/sidechain/impl/ChainListener.cpp @@ -0,0 +1,912 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { +namespace sidechain { + +class Federator; + +ChainListener::ChainListener( + IsMainchain isMainchain, + AccountID const& account, + std::weak_ptr&& federator, + beast::Journal j) + : isMainchain_{isMainchain == IsMainchain::yes} + , doorAccount_{account} + , doorAccountStr_{toBase58(account)} + , federator_{std::move(federator)} + , initialSync_{std::make_unique(federator_, isMainchain_, j)} + , j_{j} +{ +} + +// destructor must be defined after WebsocketClient size is known (i.e. it can +// not be defaulted in the header or the unique_ptr declration of +// WebsocketClient won't work) +ChainListener::~ChainListener() = default; + +std::string const& +ChainListener::chainName() const +{ + // Note: If this function is ever changed to return a value instead of a + // ref, review the code to ensure the "jv" functions don't bind to temps + static const std::string m("Mainchain"); + static const std::string s("Sidechain"); + return isMainchain_ ? m : s; +} + +namespace detail { +// consider making this available as a general utility +// Run a lambda on scope exit, unless the `reset` function is called. +template +[[nodiscard]] inline auto +make_scope(F f) +{ + static int dummy = 0; + auto d = [f = std::move(f)](auto) { f(); }; + return std::unique_ptr{&dummy, std::move(d)}; +} + +template +std::optional +getMemoData(Json::Value const& v, std::uint32_t index) = delete; + +template <> +std::optional +getMemoData(Json::Value const& v, std::uint32_t index) +{ + try + { + uint256 result; + if (result.parseHex( + v[jss::Memos][index][jss::Memo][jss::MemoData].asString())) + return result; + } + catch (...) + { + } + return {}; +} + +template <> +std::optional +getMemoData(Json::Value const& v, std::uint32_t index) +{ + try + { + auto const hexData = + v[jss::Memos][index][jss::Memo][jss::MemoData].asString(); + auto d = hexData.data(); + if (hexData.size() != 2) + return {}; + auto highNibble = charUnHex(d[0]); + auto lowNibble = charUnHex(d[1]); + if (highNibble < 0 || lowNibble < 0) + return {}; + return (highNibble << 4) | lowNibble; + } + catch (...) + { + } + return {}; +} + +} // namespace detail + +template +void +ChainListener::pushEvent( + E&& e, + int txHistoryIndex, + std::lock_guard const&) +{ + static_assert(std::is_rvalue_reference_v, ""); + + if (initialSync_) + { + auto const hasReplayed = initialSync_->onEvent(std::move(e)); + if (hasReplayed) + initialSync_.reset(); + } + else if (auto f = federator_.lock(); f && txHistoryIndex >= 0) + { + f->push(std::move(e)); + } +} + +void +ChainListener::processMessage(Json::Value const& msg) +{ + // Even though this lock has a large scope, this function does very little + // processing and should run relatively quickly + std::lock_guard l{m_}; + + JLOGV( + j_.trace(), + "chain listener message", + jv("msg", msg), + jv("isMainchain", isMainchain_)); + + if (!msg.isMember(jss::validated) || !msg[jss::validated].asBool()) + { + JLOGV( + j_.trace(), + "ignoring listener message", + jv("reason", "not validated"), + jv("msg", msg), + jv("chain_name", chainName())); + return; + } + + if (!msg.isMember(jss::engine_result_code)) + { + JLOGV( + j_.trace(), + "ignoring listener message", + jv("reason", "no engine result code"), + jv("msg", msg), + jv("chain_name", chainName())); + return; + } + + if (!msg.isMember(jss::account_history_tx_index)) + { + JLOGV( + j_.trace(), + "ignoring listener message", + jv("reason", "no account history tx index"), + jv("msg", msg), + jv("chain_name", chainName())); + return; + } + + if (!msg.isMember(jss::meta)) + { + JLOGV( + j_.trace(), + "ignoring listener message", + jv("reason", "tx meta"), + jv("msg", msg), + jv("chain_name", chainName())); + return; + } + + auto fieldMatchesStr = + [](Json::Value const& val, char const* field, char const* toMatch) { + if (!val.isMember(field)) + return false; + auto const f = val[field]; + if (!f.isString()) + return false; + return f.asString() == toMatch; + }; + + bool const isLastInHistory = [&msg] { + if (msg.isMember(jss::account_history_tx_last)) + return msg[jss::account_history_tx_last].asBool(); + return false; + }(); + + // no matter how this function exits, run this code. It handles the + // "lastInHistory" condition. It is difficult to see how to property + // annotate `make_scope` for clang's thread safety analysis, so it is + // skipped. + auto onExit = detail::make_scope([&]() NO_THREAD_SAFETY_ANALYSIS { + if (!isLastInHistory) + return; + + if (initialSync_ && isLastInHistory) + { + event::StartOfHistoricTransactions e{isMainchain_}; + auto const hasReplayed = initialSync_->onEvent(std::move(e)); + if (hasReplayed) + initialSync_.reset(); + + JLOGV( + j_.trace(), + "sent start of historic transactions event", + jv("isMainchain", isMainchain_), + jv("hasReplayed", hasReplayed)); + } + else + { + // Note this branch is needed since the other chain's + // "setNoLastXChainTxnWithResult" may reset the initialSync object. + if (auto f = federator_.lock()) + { + // Inform the other sync object that the last transaction + // with a result was found. Note that if start of historic + // transactions is found while listening to the mainchain, the + // _sidechain_ listener needs to be informed that there is no + // last cross chain transaction with result. + Federator::ChainType const chainType = + getChainType(!isMainchain_); + f->setNoLastXChainTxnWithResult(chainType); + } + } + }); + + TER const txnTER = [&msg] { + return TER::fromInt(msg[jss::engine_result_code].asInt()); + }(); + + bool const txnSuccess = (txnTER == tesSUCCESS); + + // values < 0 are historical txns. values >= 0 are new transactions. Only + // the initial sync needs historical txns. + int const txnHistoryIndex = msg[jss::account_history_tx_index].asInt(); + + auto const meta = msg[jss::meta]; + + // There are two payment types of interest: + // 1. User initiated payments on this chain that trigger a transaction on + // the other chain. + // 2. Federated initated payments on this chain whose status needs to be + // checked. + enum class PaymentType { user, federator }; + auto paymentTypeOpt = [&]() -> std::optional { + // Only keep transactions to or from the door account. + // Transactions to the account are initiated by users and are are cross + // chain transactions. Transaction from the account are initiated by + // federators and need to be monitored for errors. There are two types + // of transactions that originate from the door account: the second half + // of a cross chain payment and a refund of a failed cross chain + // payment. + + if (!fieldMatchesStr(msg, jss::type, jss::transaction)) + return {}; + + if (!msg.isMember(jss::transaction)) + return {}; + auto const txn = msg[jss::transaction]; + + if (!fieldMatchesStr(txn, jss::TransactionType, "Payment")) + return {}; + + bool const accIsSrc = + fieldMatchesStr(txn, jss::Account, doorAccountStr_.c_str()); + bool const accIsDst = + fieldMatchesStr(txn, jss::Destination, doorAccountStr_.c_str()); + + if (accIsSrc == accIsDst) + { + // either account is not involved, or self send + return {}; + } + + if (accIsSrc) + return PaymentType::federator; + return PaymentType::user; + }(); + + // There are four types of messages used to control the federator accounts: + // 1. AccountSet without modifying account settings. These txns are used to + // trigger TicketCreate txns. + // 2. TicketCreate to issue tickets. + // 3. AccountSet that changes the depositAuth setting of accounts. + // 4. SignerListSet to update the signerList of accounts. + // 5. AccoutSet that disables the master key. All transactions before this + // are used for setup only and should be ignored. This transaction is also + // used to help set the initial transaction sequence numbers + enum class AccountControlType { + trigger, + ticket, + depositAuth, + signerList, + disableMasterKey + }; + auto accountControlTypeOpt = [&]() -> std::optional { + if (!fieldMatchesStr(msg, jss::type, jss::transaction)) + return {}; + + if (!msg.isMember(jss::transaction)) + return {}; + auto const txn = msg[jss::transaction]; + + if (fieldMatchesStr(txn, jss::TransactionType, "AccountSet")) + { + if (!(txn.isMember(jss::SetFlag) || txn.isMember(jss::ClearFlag))) + { + return AccountControlType::trigger; + } + else + { + // Get the flags value at the key. If the key is not present, + // return 0. + auto getFlags = + [&txn](Json::StaticString const& key) -> std::uint32_t { + if (txn.isMember(key)) + { + auto const val = txn[key]; + try + { + return val.asUInt(); + } + catch (...) + { + } + } + return 0; + }; + + std::uint32_t const setFlags = getFlags(jss::SetFlag); + std::uint32_t const clearFlags = getFlags(jss::ClearFlag); + + if (setFlags == asfDepositAuth || clearFlags == asfDepositAuth) + return AccountControlType::depositAuth; + + if (setFlags == asfDisableMaster) + return AccountControlType::disableMasterKey; + } + } + if (fieldMatchesStr(txn, jss::TransactionType, "TicketCreate")) + return AccountControlType::ticket; + if (fieldMatchesStr(txn, jss::TransactionType, "SignerListSet")) + return AccountControlType::signerList; + + return {}; + }(); + + if (!paymentTypeOpt && !accountControlTypeOpt) + { + JLOGV( + j_.warn(), + "ignoring listener message", + jv("reason", "wrong type, not payment nor account control tx"), + jv("msg", msg), + jv("chain_name", chainName())); + return; + } + assert(!paymentTypeOpt || !accountControlTypeOpt); + + auto const txnHash = [&]() -> std::optional { + try + { + uint256 result; + if (result.parseHex(msg[jss::transaction][jss::hash].asString())) + return result; + } + catch (...) + { + } + // TODO: this is an insane input stream + // Detect and connect to another server + return {}; + }(); + if (!txnHash) + { + JLOG(j_.warn()) << "ignoring listener message, no tx hash"; + return; + } + + auto const seq = [&]() -> std::optional { + try + { + return msg[jss::transaction][jss::Sequence].asUInt(); + } + catch (...) + { + // TODO: this is an insane input stream + // Detect and connect to another server + return {}; + } + }(); + if (!seq) + { + JLOG(j_.warn()) << "ignoring listener message, no tx seq"; + return; + } + + if (paymentTypeOpt) + { + PaymentType const paymentType = *paymentTypeOpt; + + std::optional deliveredAmt; + if (meta.isMember(jss::delivered_amount)) + { + deliveredAmt = + amountFromJson(sfGeneric, meta[jss::delivered_amount]); + } + + auto const src = [&]() -> std::optional { + try + { + return parseBase58( + msg[jss::transaction][jss::Account].asString()); + } + catch (...) + { + } + // TODO: this is an insane input stream + // Detect and connect to another server + return {}; + }(); + if (!src) + { + // TODO: handle the error + return; + } + + auto const dst = [&]() -> std::optional { + try + { + switch (paymentType) + { + case PaymentType::user: { + // This is the destination of the "other chain" + // transfer, which is specified as a memo. + if (!msg.isMember(jss::transaction)) + { + return std::nullopt; + } + try + { + // the memo data is a hex encoded version of the + // base58 encoded address. This was chosen for ease + // of encoding by clients. + auto const hexData = + msg[jss::transaction][jss::Memos][0u][jss::Memo] + [jss::MemoData] + .asString(); + if ((hexData.size() > 100) || (hexData.size() % 2)) + return std::nullopt; + + auto const asciiData = [&]() -> std::string { + std::string result; + result.reserve(40); + auto d = hexData.data(); + for (int i = 0; i < hexData.size(); i += 2) + { + auto highNibble = charUnHex(d[i]); + auto lowNibble = charUnHex(d[i + 1]); + if (highNibble < 0 || lowNibble < 0) + return {}; + char c = (highNibble << 4) | lowNibble; + result.push_back(c); + } + return result; + }(); + return parseBase58(asciiData); + } + catch (...) + { + // User did not specify a destination address in a + // memo + return std::nullopt; + } + } + case PaymentType::federator: + return parseBase58( + msg[jss::transaction][jss::Destination].asString()); + } + } + catch (...) + { + } + // TODO: this is an insane input stream + // Detect and connect to another server + return {}; + }(); + if (!dst) + { + // TODO: handle the error + return; + } + + switch (paymentType) + { + case PaymentType::federator: { + auto s = txnSuccess ? j_.trace() : j_.error(); + char const* status = txnSuccess ? "success" : "fail"; + JLOGV( + s, + "federator txn status", + jv("chain_name", chainName()), + jv("status", status), + jv("msg", msg)); + + auto const txnTypeRaw = + detail::getMemoData(msg[jss::transaction], 0); + + if (!txnTypeRaw || *txnTypeRaw > Federator::txnTypeLast) + { + JLOGV( + j_.fatal(), + "expected valid txnType in ChainListener", + jv("msg", msg)); + return; + } + + Federator::TxnType const txnType = + static_cast(*txnTypeRaw); + + auto const srcChainTxnHash = + detail::getMemoData(msg[jss::transaction], 1); + + if (!srcChainTxnHash) + { + JLOGV( + j_.fatal(), + "expected srcChainTxnHash in ChainListener", + jv("msg", msg)); + return; + } + static_assert( + Federator::txnTypeLast == 2, "Add new case below"); + switch (txnType) + { + case Federator::TxnType::xChain: { + using namespace event; + // The dirction looks backwards, but it's not. The + // direction is for the *triggering* transaction. + auto const dir = + isMainchain_ ? Dir::sideToMain : Dir::mainToSide; + XChainTransferResult e{ + dir, + *dst, + deliveredAmt, + *seq, + *srcChainTxnHash, + *txnHash, + txnTER, + txnHistoryIndex}; + pushEvent(std::move(e), txnHistoryIndex, l); + } + break; + case Federator::TxnType::refund: { + using namespace event; + // The direction is for the triggering transaction. + auto const dir = + isMainchain_ ? Dir::mainToSide : Dir::sideToMain; + auto const dstChainTxnHash = + detail::getMemoData( + msg[jss::transaction], 2); + if (!dstChainTxnHash) + { + JLOGV( + j_.fatal(), + "expected valid dstChainTxnHash in " + "ChainListener", + jv("msg", msg)); + return; + } + RefundTransferResult e{ + dir, + *dst, + deliveredAmt, + *seq, + *srcChainTxnHash, + *dstChainTxnHash, + *txnHash, + txnTER, + txnHistoryIndex}; + pushEvent(std::move(e), txnHistoryIndex, l); + } + break; + } + } + break; + case PaymentType::user: { + if (!txnSuccess) + return; + + if (!deliveredAmt) + return; + { + using namespace event; + XChainTransferDetected e{ + isMainchain_ ? Dir::mainToSide : Dir::sideToMain, + *src, + *dst, + *deliveredAmt, + *seq, + *txnHash, + txnHistoryIndex}; + pushEvent(std::move(e), txnHistoryIndex, l); + } + } + break; + } + } + else + { + // account control tx + auto const ledgerIndex = [&]() -> std::optional { + try + { + return msg["ledger_index"].asInt(); + } + catch (...) + { + JLOGV(j_.error(), "no ledger_index", jv("message", msg)); + assert(false); + return {}; + } + }(); + if (!ledgerIndex) + { + JLOG(j_.warn()) << "ignoring listener message, no ledgerIndex"; + return; + } + + auto const getSourceTag = [&]() -> std::optional { + try + { + return msg[jss::transaction]["SourceTag"].asUInt(); + } + catch (...) + { + JLOGV(j_.error(), "wrong SourceTag", jv("message", msg)); + assert(false); + return {}; + } + }; + + auto const getMemoStr = [&](std::uint32_t index) -> std::string { + try + { + if (msg[jss::transaction][jss::Memos][index] == + Json::Value::null) + return {}; + auto str = std::string(msg[jss::transaction][jss::Memos][index] + [jss::Memo][jss::MemoData] + .asString()); + assert(str.length() <= event::MemoStringMax); + return str; + } + catch (...) + { + } + return {}; + }; + + auto const accountControlType = *accountControlTypeOpt; + switch (accountControlType) + { + case AccountControlType::trigger: { + JLOGV( + j_.trace(), + "AccountControlType::trigger", + jv("chain_name", chainName()), + jv("account_seq", *seq), + jv("msg", msg)); + auto sourceTag = getSourceTag(); + if (!sourceTag) + { + JLOG(j_.warn()) + << "ignoring listener message, no sourceTag"; + return; + } + auto memoStr = getMemoStr(0); + event::TicketCreateTrigger e = { + isMainchain_ ? event::Dir::mainToSide + : event::Dir::sideToMain, + txnSuccess, + 0, + *ledgerIndex, + *txnHash, + txnHistoryIndex, + *sourceTag, + std::move(memoStr)}; + pushEvent(std::move(e), txnHistoryIndex, l); + break; + } + case AccountControlType::ticket: { + JLOGV( + j_.trace(), + "AccountControlType::ticket", + jv("chain_name", chainName()), + jv("account_seq", *seq), + jv("msg", msg)); + auto sourceTag = getSourceTag(); + if (!sourceTag) + { + JLOG(j_.warn()) + << "ignoring listener message, no sourceTag"; + return; + } + + auto const triggeringTxnHash = + detail::getMemoData(msg[jss::transaction], 0); + if (!triggeringTxnHash) + { + JLOGV( + (txnSuccess ? j_.trace() : j_.error()), + "bootstrap ticket", + jv("chain_name", chainName()), + jv("account_seq", *seq), + jv("msg", msg)); + + if (!txnSuccess) + return; + + event::BootstrapTicket e = { + isMainchain_, + txnSuccess, + *seq, + *ledgerIndex, + txnHistoryIndex, + *sourceTag}; + pushEvent(std::move(e), txnHistoryIndex, l); + return; + } + + // The TicketCreate tx is both the result of its triggering + // AccountSet tx, and the trigger of another account control tx, + // if there is a tx in the memo field. + event::TicketCreateResult e = { + isMainchain_ ? event::Dir::sideToMain + : event::Dir::mainToSide, + txnSuccess, + *seq, + *ledgerIndex, + *triggeringTxnHash, + *txnHash, + txnHistoryIndex, + *sourceTag, + getMemoStr(1)}; + pushEvent(std::move(e), txnHistoryIndex, l); + break; + } + case AccountControlType::depositAuth: { + JLOGV( + j_.trace(), + "AccountControlType::depositAuth", + jv("chain_name", chainName()), + jv("account_seq", *seq), + jv("msg", msg)); + auto const triggeringTxHash = + detail::getMemoData(msg[jss::transaction], 0); + if (!triggeringTxHash) + { + JLOG(j_.warn()) + << "ignoring listener message, no triggeringTxHash"; + return; + } + + auto opOpt = [&]() -> std::optional { + try + { + if (msg[jss::transaction].isMember(jss::SetFlag) && + msg[jss::transaction][jss::SetFlag].isIntegral()) + { + assert( + msg[jss::transaction][jss::SetFlag].asUInt() == + asfDepositAuth); + return event::AccountFlagOp::set; + } + if (msg[jss::transaction].isMember(jss::ClearFlag) && + msg[jss::transaction][jss::ClearFlag].isIntegral()) + { + assert( + msg[jss::transaction][jss::ClearFlag] + .asUInt() == asfDepositAuth); + + return event::AccountFlagOp::clear; + } + } + catch (...) + { + } + JLOGV( + j_.error(), + "unexpected accountSet tx", + jv("message", msg)); + assert(false); + return {}; + }(); + if (!opOpt) + return; + + event::DepositAuthResult e{ + isMainchain_ ? event::Dir::sideToMain + : event::Dir::mainToSide, + txnSuccess, + *seq, + *ledgerIndex, + *triggeringTxHash, + txnHistoryIndex, + *opOpt}; + pushEvent(std::move(e), txnHistoryIndex, l); + break; + } + case AccountControlType::signerList: + // TODO + break; + case AccountControlType::disableMasterKey: { + event::DisableMasterKeyResult e{ + isMainchain_, *seq, txnHistoryIndex}; + pushEvent(std::move(e), txnHistoryIndex, l); + break; + } + break; + } + } + + // Note: Handling "last in history" is done through the lambda given + // to `make_scope` earlier in the function +} + +void +ChainListener::setLastXChainTxnWithResult(uint256 const& hash) +{ + // Note that `onMessage` also locks this mutex, and it calls + // `setLastXChainTxnWithResult`. However, it calls that function on the + // other chain, so the mutex will not be locked twice on the same + // thread. + std::lock_guard l{m_}; + if (!initialSync_) + return; + + auto const hasReplayed = initialSync_->setLastXChainTxnWithResult(hash); + if (hasReplayed) + initialSync_.reset(); +} + +void +ChainListener::setNoLastXChainTxnWithResult() +{ + // Note that `onMessage` also locks this mutex, and it calls + // `setNoLastXChainTxnWithResult`. However, it calls that function on + // the other chain, so the mutex will not be locked twice on the same + // thread. + std::lock_guard l{m_}; + if (!initialSync_) + return; + + bool const hasReplayed = initialSync_->setNoLastXChainTxnWithResult(); + if (hasReplayed) + initialSync_.reset(); +} + +Json::Value +ChainListener::getInfo() const +{ + std::lock_guard l{m_}; + + Json::Value ret{Json::objectValue}; + ret[jss::state] = initialSync_ ? "syncing" : "normal"; + if (initialSync_) + { + ret[jss::sync_info] = initialSync_->getInfo(); + } + // get the state (in sync, syncing) + return ret; +} + +} // namespace sidechain +} // namespace ripple diff --git a/src/ripple/app/sidechain/impl/ChainListener.h b/src/ripple/app/sidechain/impl/ChainListener.h new file mode 100644 index 0000000000..dddc316bb5 --- /dev/null +++ b/src/ripple/app/sidechain/impl/ChainListener.h @@ -0,0 +1,102 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_IMPL_CHAINLISTENER_H_INCLUDED +#define RIPPLE_SIDECHAIN_IMPL_CHAINLISTENER_H_INCLUDED + +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace ripple { +namespace sidechain { + +class Federator; +class WebsocketClient; + +class ChainListener +{ +protected: + enum class IsMainchain { no, yes }; + + bool const isMainchain_; + // Sending xrp to the door account will trigger a x-chain transaction + AccountID const doorAccount_; + std::string const doorAccountStr_; + std::weak_ptr federator_; + mutable std::mutex m_; + // Logic to handle potentially collecting and replaying historical + // transactions. Will be empty after replaying. + std::unique_ptr GUARDED_BY(m_) initialSync_; + beast::Journal j_; + + ChainListener( + IsMainchain isMainchain, + AccountID const& account, + std::weak_ptr&& federator, + beast::Journal j); + + virtual ~ChainListener(); + + std::string const& + chainName() const; + + void + processMessage(Json::Value const& msg) EXCLUDES(m_); + + template + void + pushEvent(E&& e, int txHistoryIndex, std::lock_guard const&) + REQUIRES(m_); + +public: + void + setLastXChainTxnWithResult(uint256 const& hash) EXCLUDES(m_); + void + setNoLastXChainTxnWithResult() EXCLUDES(m_); + + Json::Value + getInfo() const EXCLUDES(m_); + + using RpcCallback = std::function; + + /** + * send a RPC and call the callback with the RPC result + * @param cmd PRC command + * @param params RPC command parameter + * @param onResponse callback to process RPC result + */ + virtual void + send( + std::string const& cmd, + Json::Value const& params, + RpcCallback onResponse) = 0; +}; + +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/app/sidechain/impl/DoorKeeper.cpp b/src/ripple/app/sidechain/impl/DoorKeeper.cpp new file mode 100644 index 0000000000..7a64fb93c6 --- /dev/null +++ b/src/ripple/app/sidechain/impl/DoorKeeper.cpp @@ -0,0 +1,327 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 +#include +#include +#include + +namespace ripple { +namespace sidechain { + +DoorKeeper::DoorKeeper( + bool isMainChain, + AccountID const& account, + TicketRunner& ticketRunner, + Federator& federator, + beast::Journal j) + : isMainChain_(isMainChain) + , accountStr_(toBase58(account)) + , ticketRunner_(ticketRunner) + , federator_(federator) + , j_(j) +{ +} + +void +DoorKeeper::init() +{ + std::lock_guard lock(mtx_); + if (initData_.status_ != InitializeStatus::waitLedger) + return; + initData_.status_ = InitializeStatus::waitAccountInfo; + rpcAccountInfo(lock); +} + +void +DoorKeeper::updateQueueLength(std::uint32_t length) +{ + DoorStatus oldStatus; + auto const tx = [&]() -> std::optional { + enum Action { setFlag, clearFlag, noAction }; + Action action = noAction; + std::lock_guard lock(mtx_); + JLOGV( + j_.trace(), + "updateQueueLength", + jv("account:", accountStr_), + jv("QLen", length), + jv("chain", (isMainChain_ ? "main" : "side"))); + + if (initData_.status_ != InitializeStatus::initialized) + return {}; + + oldStatus = status_; + if (length >= HighWaterMark && status_ == DoorStatus::open) + { + action = setFlag; + status_ = DoorStatus::closing; + } + else if (length <= LowWaterMark && status_ == DoorStatus::closed) + { + action = clearFlag; + status_ = DoorStatus::opening; + } + + if (action == noAction) + return {}; + + XRPAmount const fee{Federator::accountControlTxFee}; + Json::Value txJson; + txJson[jss::TransactionType] = "AccountSet"; + txJson[jss::Account] = accountStr_; + txJson[jss::Sequence] = 0; // to be filled by ticketRunner + txJson[jss::Fee] = to_string(fee); + if (action == setFlag) + txJson[jss::SetFlag] = asfDepositAuth; + else + txJson[jss::ClearFlag] = asfDepositAuth; + return txJson; + }(); + + if (tx) + { + bool triggered = false; + if (isMainChain_) + { + triggered = + ticketRunner_.trigger(TicketPurpose::mainDoorKeeper, tx, {}); + } + else + { + triggered = + ticketRunner_.trigger(TicketPurpose::sideDoorKeeper, {}, tx); + } + + JLOGV( + j_.trace(), + "updateQueueLength", + jv("account:", accountStr_), + jv("QLen", length), + jv("chain", (isMainChain_ ? "main" : "side")), + jv("tx", *tx), + jv("triggered", (triggered ? "yes" : "no"))); + + if (!triggered) + { + std::lock_guard lock(mtx_); + status_ = oldStatus; + } + } +} + +void +DoorKeeper::onEvent(const event::DepositAuthResult& e) +{ + std::lock_guard lock(mtx_); + if (initData_.status_ != InitializeStatus::initialized) + { + JLOG(j_.trace()) << "Queue an event"; + initData_.toReplay_.push(e); + } + else + { + processEvent(e, lock); + } +} + +void +DoorKeeper::rpcAccountInfo(std::lock_guard const&) +{ + Json::Value params = [&] { + Json::Value r; + r[jss::account] = accountStr_; + r[jss::ledger_index] = "validated"; + r[jss::signer_lists] = false; + return r; + }(); + + rpcChannel_->send( + "account_info", + params, + [chain = isMainChain_ ? Federator::mainChain : Federator::sideChain, + wp = federator_.weak_from_this()](Json::Value const& response) { + if (auto f = wp.lock()) + f->getDoorKeeper(chain).accountInfoResult(response); + }); +} + +void +DoorKeeper::accountInfoResult(const Json::Value& rpcResult) +{ + auto ledgerNFlagsOpt = + [&]() -> std::optional> { + try + { + if (rpcResult.isMember(jss::error)) + { + return {}; + } + if (!rpcResult[jss::validated].asBool()) + { + return {}; + } + if (rpcResult[jss::account_data][jss::Account] != accountStr_) + { + return {}; + } + if (!rpcResult[jss::account_data][jss::Flags].isIntegral()) + { + return {}; + } + if (!rpcResult[jss::ledger_index].isIntegral()) + { + return {}; + } + + return std::make_pair( + rpcResult[jss::ledger_index].asUInt(), + rpcResult[jss::account_data][jss::Flags].asUInt()); + } + catch (...) + { + return {}; + } + }(); + + if (!ledgerNFlagsOpt) + { + // should not reach here since we only ask account_object after a + // validated ledger + JLOGV(j_.error(), "account_info result ", jv("result", rpcResult)); + assert(false); + return; + } + + auto [ledgerIndex, flags] = *ledgerNFlagsOpt; + { + JLOGV( + j_.trace(), + "accountInfoResult", + jv("ledgerIndex", ledgerIndex), + jv("flags", flags)); + std::lock_guard lock(mtx_); + initData_.ledgerIndex_ = ledgerIndex; + status_ = (flags & lsfDepositAuth) == 0 ? DoorStatus::open + : DoorStatus::closed; + while (!initData_.toReplay_.empty()) + { + processEvent(initData_.toReplay_.front(), lock); + initData_.toReplay_.pop(); + } + initData_.status_ = InitializeStatus::initialized; + JLOG(j_.info()) << "DoorKeeper initialized, status " + << (status_ == DoorStatus::open ? "open" : "closed"); + } +} + +void +DoorKeeper::processEvent( + const event::DepositAuthResult& e, + std::lock_guard const&) +{ + if (e.ledgerIndex_ <= initData_.ledgerIndex_) + { + JLOGV( + j_.trace(), + "DepositAuthResult, ignoring an old result", + jv("account:", accountStr_), + jv("operation", + (e.op_ == event::AccountFlagOp::set ? "set" : "clear"))); + return; + } + + JLOGV( + j_.trace(), + "DepositAuthResult", + jv("chain", (isMainChain_ ? "main" : "side")), + jv("account:", accountStr_), + jv("operation", + (e.op_ == event::AccountFlagOp::set ? "set" : "clear"))); + + if (!e.success_) + { + JLOG(j_.error()) << "DepositAuthResult event error, account " + << (isMainChain_ ? "main" : "side") << accountStr_; + assert(false); + return; + } + + switch (e.op_) + { + case event::AccountFlagOp::set: + assert( + status_ == DoorStatus::open || status_ == DoorStatus::closing); + status_ = DoorStatus::closed; + break; + case event::AccountFlagOp::clear: + assert( + status_ == DoorStatus::closed || + status_ == DoorStatus::opening); + status_ = DoorStatus::open; + break; + } +} + +Json::Value +DoorKeeper::getInfo() const +{ + auto DoorStatusToStr = [](DoorKeeper::DoorStatus s) -> std::string { + switch (s) + { + case DoorKeeper::DoorStatus::open: + return "open"; + case DoorKeeper::DoorStatus::opening: + return "opening"; + case DoorKeeper::DoorStatus::closed: + return "closed"; + case DoorKeeper::DoorStatus::closing: + return "closing"; + } + return {}; + }; + + Json::Value ret{Json::objectValue}; + { + std::lock_guard lock{mtx_}; + if (initData_.status_ == InitializeStatus::initialized) + { + ret["initialized"] = "true"; + ret["status"] = DoorStatusToStr(status_); + } + else + { + ret["initialized"] = "false"; + } + } + return ret; +} + +void +DoorKeeper::setRpcChannel(std::shared_ptr channel) +{ + rpcChannel_ = std::move(channel); +} + +} // namespace sidechain +} // namespace ripple diff --git a/src/ripple/app/sidechain/impl/DoorKeeper.h b/src/ripple/app/sidechain/impl/DoorKeeper.h new file mode 100644 index 0000000000..113dd5689e --- /dev/null +++ b/src/ripple/app/sidechain/impl/DoorKeeper.h @@ -0,0 +1,128 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_IMPL_DOOR_OPENER_H +#define RIPPLE_SIDECHAIN_IMPL_DOOR_OPENER_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace ripple { +namespace sidechain { + +class TicketRunner; +class Federator; + +class DoorKeeper +{ +public: + static constexpr std::uint32_t LowWaterMark = 0; + static constexpr std::uint32_t HighWaterMark = 100; + static_assert(HighWaterMark > LowWaterMark); + enum class DoorStatus { open, closing, closed, opening }; + +private: + enum class InitializeStatus { waitLedger, waitAccountInfo, initialized }; + struct InitializeData + { + InitializeStatus status_ = InitializeStatus::waitLedger; + std::queue toReplay_; + std::uint32_t ledgerIndex_ = 0; + }; + + std::shared_ptr rpcChannel_; + bool const isMainChain_; + std::string const accountStr_; + mutable std::mutex mtx_; + InitializeData GUARDED_BY(mtx_) initData_; + DoorStatus GUARDED_BY(mtx_) status_; + TicketRunner& ticketRunner_; + Federator& federator_; + beast::Journal j_; + +public: + DoorKeeper( + bool isMainChain, + AccountID const& account, + TicketRunner& ticketRunner, + Federator& federator, + beast::Journal j); + ~DoorKeeper() = default; + + /** + * start to initialize the doorKeeper by sending accountInfo RPC + */ + void + init() EXCLUDES(mtx_); + + /** + * process the accountInfo result and set the door status + * This is the end of initialization + * + * @param rpcResult the accountInfo result + */ + void + accountInfoResult(Json::Value const& rpcResult) EXCLUDES(mtx_); + + /** + * update the doorKeeper about the number of pending XChain payments + * The doorKeeper will close the door if there are too many + * pending XChain payments and reopen the door later + * + * @param length the number of pending XChain payments + */ + void + updateQueueLength(std::uint32_t length) EXCLUDES(mtx_); + + /** + * process a DepositAuthResult event and set the door status. + * It queues the event if the doorKeeper is not yet initialized. + * + * @param e the DepositAuthResult event + */ + void + onEvent(event::DepositAuthResult const& e) EXCLUDES(mtx_); + + Json::Value + getInfo() const EXCLUDES(mtx_); + + void + setRpcChannel(std::shared_ptr channel); + +private: + void + rpcAccountInfo(std::lock_guard const&) REQUIRES(mtx_); + + void + processEvent( + event::DepositAuthResult const& e, + std::lock_guard const&) REQUIRES(mtx_); +}; + +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/app/sidechain/impl/InitialSync.cpp b/src/ripple/app/sidechain/impl/InitialSync.cpp new file mode 100644 index 0000000000..24ca11529d --- /dev/null +++ b/src/ripple/app/sidechain/impl/InitialSync.cpp @@ -0,0 +1,555 @@ + +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 +#include + +#include + +namespace ripple { +namespace sidechain { + +InitialSync::InitialSync( + std::weak_ptr federator, + bool isMainchain, + beast::Journal j) + : federator_{std::move(federator)}, isMainchain_{isMainchain}, j_{j} +{ +} + +bool +InitialSync::hasTransaction( + uint256 const& txnHash, + std::lock_guard const&) const +{ + return seenTriggeringTxns_.count(txnHash); +} + +bool +InitialSync::canReplay(std::lock_guard const&) const +{ + return !( + needsLastXChainTxn_ || needsOtherChainLastXChainTxn_ || + needsReplayStartTxnHash_); +} + +void +InitialSync::stopHistoricalTxns(std::lock_guard const&) +{ + if (!acquiringHistoricData_) + return; + + acquiringHistoricData_ = false; + if (auto f = federator_.lock()) + { + f->stopHistoricalTxns(getChainType(isMainchain_)); + } +} + +void +InitialSync::done() +{ + if (auto f = federator_.lock()) + { + f->initialSyncDone( + isMainchain_ ? Federator::ChainType::mainChain + : Federator::ChainType::sideChain); + } +} + +bool +InitialSync::setLastXChainTxnWithResult(uint256 const& hash) +{ + std::lock_guard l{m_}; + JLOGV( + j_.trace(), + "last xchain txn with result", + jv("needsOtherChainLastXChainTxn", needsOtherChainLastXChainTxn_), + jv("isMainchain", isMainchain_), + jv("hash", hash)); + assert(lastXChainTxnWithResult_.value_or(hash) == hash); + if (hasReplayed_ || lastXChainTxnWithResult_) + return hasReplayed_; + + lastXChainTxnWithResult_ = hash; + needsReplayStartTxnHash_ = false; + if (needsLastXChainTxn_) + { + needsLastXChainTxn_ = + !seenTriggeringTxns_.count(*lastXChainTxnWithResult_); + } + + if (!acquiringHistoricData_ && needsLastXChainTxn_) + LogicError("Initial sync could not find historic XChain transaction"); + + if (canReplay(l)) + replay(l); + + return hasReplayed_; +} + +bool +InitialSync::setNoLastXChainTxnWithResult() +{ + std::lock_guard l{m_}; + JLOGV( + j_.trace(), + "no last xchain txn with result", + jv("needsOtherChainLastXChainTxn", needsOtherChainLastXChainTxn_), + jv("isMainchain", isMainchain_)); + assert(!lastXChainTxnWithResult_); + if (hasReplayed_) + return hasReplayed_; + + needsLastXChainTxn_ = false; + needsReplayStartTxnHash_ = false; + + if (canReplay(l)) + replay(l); + + return hasReplayed_; +} + +void +InitialSync::replay(std::lock_guard const& l) +{ + if (hasReplayed_) + return; + + assert(canReplay(l)); + + // Note that this function may push a large number of events to the + // federator, and it runs under a lock. However, pushing an event to the + // federator just copies it into a collection (it does not handle the event + // in the same thread). So this should run relatively quickly. + stopHistoricalTxns(l); + hasReplayed_ = true; + JLOGV( + j_.trace(), + "InitialSync replay,", + jv("chain_name", (isMainchain_ ? "Mainchain" : "Sidechain")), + jv("lastXChainTxnWithResult_", + (lastXChainTxnWithResult_ ? strHex(*lastXChainTxnWithResult_) + : "not set"))); + + if (lastXChainTxnWithResult_) + assert(seenTriggeringTxns_.count(*lastXChainTxnWithResult_)); + + if (lastXChainTxnWithResult_ && + seenTriggeringTxns_.count(*lastXChainTxnWithResult_)) + { + // Remove the XChainTransferDetected event associated with this txn, and + // all the XChainTransferDetected events before it. They have already + // been submitted. If they are not removed, they will never collect + // enough signatures to be submitted (since the other federators have + // already submitted it), and it will prevent subsequent event from + // replaying. + std::vector toRemove; + toRemove.reserve(pendingEvents_.size()); + std::vector toRemoveTrigger; + bool matched = false; + for (auto i = pendingEvents_.cbegin(), e = pendingEvents_.cend(); + i != e; + ++i) + { + auto const et = eventType(i->second); + if (et == event::EventType::trigger) + { + toRemove.push_back(i); + } + else if (et == event::EventType::resultAndTrigger) + { + toRemoveTrigger.push_back(i); + } + else + { + continue; + } + + auto const txnHash = sidechain::txnHash(i->second); + if (!txnHash) + { + // All triggering events should have a txnHash + assert(0); + continue; + } + JLOGV( + j_.trace(), + "InitialSync replay, remove trigger event from pendingEvents_", + jv("chain_name", (isMainchain_ ? "Mainchain" : "Sidechain")), + jv("txnHash", *txnHash)); + if (*lastXChainTxnWithResult_ == *txnHash) + { + matched = true; + break; + } + } + assert(matched); + if (matched) + { + for (auto i : toRemoveTrigger) + { + if (auto ticketResult = std::get_if( + &(pendingEvents_.erase(i, i)->second)); + ticketResult) + { + ticketResult->removeTrigger(); + } + } + for (auto i = toRemove.begin(), e = toRemove.end(); i != e; ++i) + { + pendingEvents_.erase(*i); + } + } + } + + if (auto f = federator_.lock()) + { + for (auto&& [_, e] : pendingEvents_) + f->push(std::move(e)); + } + + seenTriggeringTxns_.clear(); + pendingEvents_.clear(); + done(); +} + +bool +InitialSync::onEvent(event::XChainTransferDetected&& e) +{ + return onTriggerEvent(std::move(e)); +} + +bool +InitialSync::onEvent(event::XChainTransferResult&& e) +{ + return onResultEvent(std::move(e), 1); +} + +bool +InitialSync::onEvent(event::TicketCreateTrigger&& e) +{ + return onTriggerEvent(std::move(e)); +} + +bool +InitialSync::onEvent(event::TicketCreateResult&& e) +{ + static_assert(std::is_rvalue_reference_v, ""); + + std::lock_guard l{m_}; + if (hasReplayed_) + { + assert(0); + return hasReplayed_; + } + + JLOGV( + j_.trace(), "InitialSync TicketCreateResult", jv("event", e.toJson())); + + if (needsOtherChainLastXChainTxn_) + { + if (auto f = federator_.lock()) + { + // Inform the other sync object that the last transaction with a + // result was found. e.dir_ is for the triggering transaction. + Federator::ChainType const chainType = srcChainType(e.dir_); + f->setLastXChainTxnWithResult( + chainType, e.txnSeq_, 2, e.srcChainTxnHash_); + } + needsOtherChainLastXChainTxn_ = false; + } + + if (!e.memoStr_.empty()) + { + seenTriggeringTxns_.insert(e.txnHash_); + if (lastXChainTxnWithResult_ && needsLastXChainTxn_) + { + if (e.txnHash_ == *lastXChainTxnWithResult_) + { + needsLastXChainTxn_ = false; + JLOGV( + j_.trace(), + "InitialSync TicketCreateResult, found the trigger tx", + jv("txHash", e.txnHash_), + jv("chain_name", + (isMainchain_ ? "Mainchain" : "Sidechain"))); + } + } + } + pendingEvents_[e.rpcOrder_] = std::move(e); + + if (canReplay(l)) + replay(l); + + return hasReplayed_; +} + +bool +InitialSync::onEvent(event::DepositAuthResult&& e) +{ + return onResultEvent(std::move(e), 1); +} + +bool +InitialSync::onEvent(event::BootstrapTicket&& e) +{ + std::lock_guard l{m_}; + + JLOGV(j_.trace(), "InitialSync onBootstrapTicket", jv("event", e.toJson())); + + if (hasReplayed_) + { + assert(0); + return hasReplayed_; + } + + pendingEvents_[e.rpcOrder_] = std::move(e); + + if (canReplay(l)) + replay(l); + + return hasReplayed_; +} + +bool +InitialSync::onEvent(event::DisableMasterKeyResult&& e) +{ + std::lock_guard l{m_}; + + if (hasReplayed_) + { + assert(0); + return hasReplayed_; + } + + JLOGV( + j_.trace(), + "InitialSync onDisableMasterKeyResultEvent", + jv("event", e.toJson())); + assert(!disableMasterKeySeq_); + disableMasterKeySeq_ = e.txnSeq_; + + pendingEvents_[e.rpcOrder_] = std::move(e); + + if (canReplay(l)) + replay(l); + + return hasReplayed_; +} + +template +bool +InitialSync::onTriggerEvent(T&& e) +{ + static_assert(std::is_rvalue_reference_v, ""); + + std::lock_guard l{m_}; + if (hasReplayed_) + { + assert(0); + return hasReplayed_; + } + + JLOGV(j_.trace(), "InitialSync onTriggerEvent", jv("event", e.toJson())); + seenTriggeringTxns_.insert(e.txnHash_); + if (lastXChainTxnWithResult_ && needsLastXChainTxn_) + { + if (e.txnHash_ == *lastXChainTxnWithResult_) + { + needsLastXChainTxn_ = false; + JLOGV( + j_.trace(), + "InitialSync onTriggerEvent, found the trigger tx", + jv("txHash", e.txnHash_), + jv("chain_name", (isMainchain_ ? "Mainchain" : "Sidechain"))); + } + } + pendingEvents_[e.rpcOrder_] = std::move(e); + + if (canReplay(l)) + { + replay(l); + } + return hasReplayed_; +} + +template +bool +InitialSync::onResultEvent(T&& e, std::uint32_t seqTook) +{ + static_assert(std::is_rvalue_reference_v, ""); + + std::lock_guard l{m_}; + if (hasReplayed_) + { + assert(0); + return hasReplayed_; + } + + JLOGV(j_.trace(), "InitialSync onResultEvent", jv("event", e.toJson())); + + if (needsOtherChainLastXChainTxn_) + { + if (auto f = federator_.lock()) + { + // Inform the other sync object that the last transaction with a + // result was found. e.dir_ is for the triggering transaction. + Federator::ChainType const chainType = srcChainType(e.dir_); + f->setLastXChainTxnWithResult( + chainType, e.txnSeq_, seqTook, e.srcChainTxnHash_); + } + needsOtherChainLastXChainTxn_ = false; + } + + pendingEvents_[e.rpcOrder_] = std::move(e); + + if (canReplay(l)) + replay(l); + + return hasReplayed_; +} + +bool +InitialSync::onEvent(event::RefundTransferResult&& e) +{ + std::lock_guard l{m_}; + if (hasReplayed_) + { + assert(0); + } + else + { + pendingEvents_[e.rpcOrder_] = std::move(e); + + if (canReplay(l)) + replay(l); + } + return hasReplayed_; +} + +bool +InitialSync::onEvent(event::StartOfHistoricTransactions&& e) +{ + std::lock_guard l{m_}; + if (lastXChainTxnWithResult_) + LogicError("Initial sync could not find historic XChain transaction"); + + if (needsOtherChainLastXChainTxn_) + { + if (auto f = federator_.lock()) + { + // Inform the other sync object that the last transaction + // with a result was found. Note that if start of historic + // transactions is found while listening to the mainchain, the + // _sidechain_ listener needs to be informed that there is no last + // cross chain transaction with result. + Federator::ChainType const chainType = getChainType(!isMainchain_); + f->setNoLastXChainTxnWithResult(chainType); + } + needsOtherChainLastXChainTxn_ = false; + } + + acquiringHistoricData_ = false; + needsOtherChainLastXChainTxn_ = false; + + if (canReplay(l)) + { + replay(l); + } + + return hasReplayed_; +} + +namespace detail { + +Json::Value +getInfo(FederatorEvent const& event) +{ + return std::visit( + [](auto const& e) { + using eventType = decltype(e); + Json::Value ret{Json::objectValue}; + if constexpr (std::is_same_v< + eventType, + event::XChainTransferDetected>) + { + ret[jss::type] = "xchain_transfer_detected"; + ret[jss::amount] = to_string(e.amt_); + ret[jss::destination_account] = to_string(e.dst_); + ret[jss::hash] = strHex(e.txnHash_); + ret[jss::sequence] = e.txnSeq_; + ret["rpc_order"] = e.rpcOrder_; + } + else if constexpr (std::is_same_v< + eventType, + event::XChainTransferResult>) + { + ret[jss::type] = "xchain_transfer_result"; + ret[jss::amount] = to_string(e.amt_); + ret[jss::destination_account] = to_string(e.dst_); + ret[jss::hash] = strHex(e.txnHash_); + ret["triggering_tx_hash"] = strHex(e.triggeringTxnHash_); + ret[jss::sequence] = e.txnSeq_; + ret[jss::result] = transHuman(e.ter_); + ret["rpc_order"] = e.rpcOrder_; + } + else + { + ret[jss::type] = "other_event"; + } + return ret; + }, + event); +} + +} // namespace detail + +Json::Value +InitialSync::getInfo() const +{ + Json::Value ret{Json::objectValue}; + { + std::lock_guard l{m_}; + ret["last_x_chain_txn_with_result"] = lastXChainTxnWithResult_ + ? strHex(*lastXChainTxnWithResult_) + : "None"; + Json::Value triggerinTxns{Json::arrayValue}; + for (auto const& h : seenTriggeringTxns_) + { + triggerinTxns.append(strHex(h)); + } + ret["seen_triggering_txns"] = triggerinTxns; + ret["needs_last_x_chain_txn"] = needsLastXChainTxn_; + ret["needs_other_chain_last_x_chain_txn"] = + needsOtherChainLastXChainTxn_; + ret["acquiring_historic_data"] = acquiringHistoricData_; + ret["needs_replay_start_txn_hash"] = needsReplayStartTxnHash_; + } + return ret; +} + +} // namespace sidechain + +} // namespace ripple diff --git a/src/ripple/app/sidechain/impl/InitialSync.h b/src/ripple/app/sidechain/impl/InitialSync.h new file mode 100644 index 0000000000..69d577917f --- /dev/null +++ b/src/ripple/app/sidechain/impl/InitialSync.h @@ -0,0 +1,214 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_IMPL_INITIALSYNC_H_INCLUDED +#define RIPPLE_SIDECHAIN_IMPL_INITIALSYNC_H_INCLUDED + +#include +#include +#include +#include + +#include + +namespace ripple { +namespace sidechain { + +class Federator; +class WebsocketClient; + +// This class handles the logic of getting a federator that joins the network +// into a "normal" state of handling new cross chain transactions and results. +// There will be two instance of this class, one for the main chain and one for +// the side chain. +// +// When a federator joins the network of other federators, the network can be in +// one of three states: +// +// 1) The initial sidechain startup. +// 2) Running normally with a quorum of federators. This federator that's +// joining just increases the quorum. +// 3) A stalled sidechain without enough federators to make forward progress. +// This federator may or may not increase the quorum enough so cross chain +// transactions can continue. In the meantime, cross chain transactions may +// continue to accumulate. +// +// No matter the state of the federators network, connecting to the network goes +// through the same steps. There are two instances of this class, one +// for the main chain and one for the side chain. +// +// The RPC command used to fetch transactions will initially be configured to +// retrieve both historical transactions and new transactions. Once the +// information needed from the historical transactions are retrieved, it will be +// changed to only stream new transactions. +// +// There are two states this class can be in: pre-replay and post-replay. In +// pre-replay mode, the class collects information from both historic and +// transactions that will be used for helping the this instance and the "other" +// instance of this class known when to stop collecting historic data, as well +// as collecting transactions for replaying. +// +// Historic data needs to be collected until: +// +// 1) The most recent historic `XChainTransferResult` event is detected (or the +// account's first transaction is detected). This is used to inform the "other" +// instance of this class which `XChainTransferDetected` event is the first that +// may need to be replayed. Since the previous `XChainTransferDetected` events +// have results on the other chain, we can definitively say the federators have +// handled these events and they don't need to be replayed. +// +// 2) Once the `lastXChainTxnWithResult_` is know, historic transactions need be +// acquired until that transaction is seen on a `XChainTransferDetected` event. +// +// Once historic data collection has completed, the collected transactions are +// replayed to the federator, and this class is not longer needed. All new +// transactions should simply be forwarded to the federator. +// +class InitialSync +{ +private: + std::weak_ptr federator_; + // Holds all the eventsseen so far. These events will be replayed to the + // federator upon switching to `normal` mode. Will be cleared while + // replaying. + std::map GUARDED_BY(m_) pendingEvents_; + // Holds all triggering cross chain transactions seen so far. This is used + // to determine if the `XChainTransferDetected` event with the + // `lastXChainTxnWithResult_` has has been seen or not. Will be cleared + // while replaying + hash_set GUARDED_BY(m_) seenTriggeringTxns_; + // Hash of the last cross chain transaction on this chain with a result on + // the "other" chain. Note: this is set when the `InitialSync` for the + // "other" chain encounters the transaction. + std::optional GUARDED_BY(m_) lastXChainTxnWithResult_; + // Track if we need to keep acquiring historic transactions for the + // `lastXChainTxnWithResult_`. This is true if the lastXChainTxnWithResult_ + // is unknown, or it is known and the transaction is not part of that + // collection yet. + bool GUARDED_BY(m_) needsLastXChainTxn_{true}; + // Track if we need to keep acquiring historic transactions for the other + // chain's `lastXChainTxnWithResult_` hash value. This is true if no + // cross chain transaction results are known and the first historical + // transaction has not been encountered. + bool GUARDED_BY(m_) needsOtherChainLastXChainTxn_{true}; + // Track if the transaction to start the replay from is known. This is true + // until `lastXChainTxnWithResult_` is known and the other listener has not + // encountered the first historical transaction + bool GUARDED_BY(m_) needsReplayStartTxnHash_{true}; + // True if the historical transactions have been replayed to the federator + bool GUARDED_BY(m_) hasReplayed_{false}; + // Track the state of the transaction data we are acquiring. + // If this is `false`, only new transactions events will be streamed. + // Note: there will be a period where this is `false` but historic txns will + // continue to come in until the rpc command has responded to the request to + // shut off historic data. + bool GUARDED_BY(m_) acquiringHistoricData_{true}; + // All transactions before "DisableMasterKey" are setup transactions and + // should be ignored + std::optional GUARDED_BY(m_) disableMasterKeySeq_; + bool const isMainchain_; + mutable std::mutex m_; + beast::Journal j_; + // See description on class for explanation of states + +public: + InitialSync( + std::weak_ptr federator, + bool isMainchain, + beast::Journal j); + + // Return `hasReplayed_`. This is used to determine if events should + // continue to be routed to this object. Once replayed, events can be + // processed normally. + [[nodiscard]] bool + setLastXChainTxnWithResult(uint256 const& hash) EXCLUDES(m_); + + // There have not been any cross chain transactions. + // Return `hasReplayed_`. This is used to determine if events should + // continue to be routed to this object. Once replayed, events can be + // processed normally. + [[nodiscard]] bool + setNoLastXChainTxnWithResult() EXCLUDES(m_); + + // Return `hasReplayed_`. This is used to determine if events should + // continue to be routed to this object. Once replayed, events can be + // processed normally. + [[nodiscard]] bool + onEvent(event::XChainTransferDetected&& e) EXCLUDES(m_); + + // Return `hasReplayed_`. This is used to determine if events should + // continue to be routed to this object. Once replayed, events can be + // processed normally. + [[nodiscard]] bool + onEvent(event::XChainTransferResult&& e) EXCLUDES(m_); + + // Return `hasReplayed_`. This is used to determine if events should + // continue to be routed to this object. Once replayed, events can be + // processed normally. + [[nodiscard]] bool + onEvent(event::RefundTransferResult&& e) EXCLUDES(m_); + + // Return `hasReplayed_`. This is used to determine if events should + // continue to be routed to this object. Once replayed, events can be + // processed normally. + [[nodiscard]] bool + onEvent(event::StartOfHistoricTransactions&& e) EXCLUDES(m_); + // Return `hasReplayed_`. + [[nodiscard]] bool + onEvent(event::TicketCreateTrigger&& e) EXCLUDES(m_); + // Return `hasReplayed_`. + [[nodiscard]] bool + onEvent(event::TicketCreateResult&& e) EXCLUDES(m_); + // Return `hasReplayed_`. + [[nodiscard]] bool + onEvent(event::DepositAuthResult&& e) EXCLUDES(m_); + [[nodiscard]] bool + onEvent(event::BootstrapTicket&& e) EXCLUDES(m_); + [[nodiscard]] bool + onEvent(event::DisableMasterKeyResult&& e) EXCLUDES(m_); + + Json::Value + getInfo() const EXCLUDES(m_); + +private: + // Replay when historical transactions are no longer being acquired, + // and the transaction to start the replay from is known. + bool + canReplay(std::lock_guard const&) const REQUIRES(m_); + void + replay(std::lock_guard const&) REQUIRES(m_); + bool + hasTransaction(uint256 const& txnHash, std::lock_guard const&) + const REQUIRES(m_); + void + stopHistoricalTxns(std::lock_guard const&) REQUIRES(m_); + template + bool + onTriggerEvent(T&& e); + template + bool + onResultEvent(T&& e, std::uint32_t seqTook); + void + done(); +}; + +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/app/sidechain/impl/MainchainListener.cpp b/src/ripple/app/sidechain/impl/MainchainListener.cpp new file mode 100644 index 0000000000..427f59b512 --- /dev/null +++ b/src/ripple/app/sidechain/impl/MainchainListener.cpp @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace sidechain { + +class Federator; + +MainchainListener::MainchainListener( + AccountID const& account, + std::weak_ptr&& federator, + beast::Journal j) + : ChainListener( + ChainListener::IsMainchain::yes, + account, + std::move(federator), + j) +{ +} +void +MainchainListener::onMessage(Json::Value const& msg) +{ + auto callbackOpt = [&]() -> std::optional { + if (msg.isMember(jss::id) && msg[jss::id].isIntegral()) + { + auto callbackId = msg[jss::id].asUInt(); + std::lock_guard lock(callbacksMtx_); + auto i = callbacks_.find(callbackId); + if (i != callbacks_.end()) + { + auto cb = i->second; + callbacks_.erase(i); + return cb; + } + } + return {}; + }(); + + if (callbackOpt) + { + JLOG(j_.trace()) << "Mainchain onMessage, reply to a callback: " << msg; + assert(msg.isMember(jss::result)); + (*callbackOpt)(msg[jss::result]); + } + else + { + processMessage(msg); + } +} + +void +MainchainListener::init( + boost::asio::io_service& ios, + boost::asio::ip::address const& ip, + std::uint16_t port) +{ + wsClient_ = std::make_unique( + [self = shared_from_this()](Json::Value const& msg) { + self->onMessage(msg); + }, + ios, + ip, + port, + /*headers*/ std::unordered_map{}, + j_); + + Json::Value params; + params[jss::account_history_tx_stream] = Json::objectValue; + params[jss::account_history_tx_stream][jss::account] = doorAccountStr_; + send("subscribe", params); +} + +// destructor must be defined after WebsocketClient size is known (i.e. it can +// not be defaulted in the header or the unique_ptr declration of +// WebsocketClient won't work) +MainchainListener::~MainchainListener() = default; + +void +MainchainListener::shutdown() +{ + if (wsClient_) + wsClient_->shutdown(); +} + +std::uint32_t +MainchainListener::send(std::string const& cmd, Json::Value const& params) +{ + return wsClient_->send(cmd, params); +} + +void +MainchainListener::stopHistoricalTxns() +{ + Json::Value params; + params[jss::stop_history_tx_only] = true; + params[jss::account_history_tx_stream] = Json::objectValue; + params[jss::account_history_tx_stream][jss::account] = doorAccountStr_; + send("unsubscribe", params); +} + +void +MainchainListener::send( + std::string const& cmd, + Json::Value const& params, + RpcCallback onResponse) +{ + JLOGV( + j_.trace(), "Mainchain send", jv("command", cmd), jv("params", params)); + + auto id = wsClient_->send(cmd, params); + std::lock_guard lock(callbacksMtx_); + callbacks_.emplace(id, onResponse); +} +} // namespace sidechain +} // namespace ripple diff --git a/src/ripple/app/sidechain/impl/MainchainListener.h b/src/ripple/app/sidechain/impl/MainchainListener.h new file mode 100644 index 0000000000..972da3cdea --- /dev/null +++ b/src/ripple/app/sidechain/impl/MainchainListener.h @@ -0,0 +1,84 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_IMPL_MAINCHAINLISTENER_H_INCLUDED +#define RIPPLE_SIDECHAIN_IMPL_MAINCHAINLISTENER_H_INCLUDED + +#include +#include +#include + +#include +#include + +#include + +namespace ripple { +namespace sidechain { + +class Federator; +class WebsocketClient; + +class MainchainListener : public ChainListener, + public std::enable_shared_from_this +{ + std::unique_ptr wsClient_; + + mutable std::mutex callbacksMtx_; + std::map GUARDED_BY(callbacksMtx_) callbacks_; + + void + onMessage(Json::Value const& msg) EXCLUDES(callbacksMtx_); + +public: + MainchainListener( + AccountID const& account, + std::weak_ptr&& federator, + beast::Journal j); + + ~MainchainListener(); + + void + init( + boost::asio::io_service& ios, + boost::asio::ip::address const& ip, + std::uint16_t port); + + // Returns command id that will be returned in the response + std::uint32_t + send(std::string const& cmd, Json::Value const& params) + EXCLUDES(callbacksMtx_); + + void + shutdown(); + + void + stopHistoricalTxns(); + + void + send( + std::string const& cmd, + Json::Value const& params, + RpcCallback onResponse) override; +}; + +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/app/sidechain/impl/SidechainListener.cpp b/src/ripple/app/sidechain/impl/SidechainListener.cpp new file mode 100644 index 0000000000..2691b80a70 --- /dev/null +++ b/src/ripple/app/sidechain/impl/SidechainListener.cpp @@ -0,0 +1,130 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace sidechain { + +SidechainListener::SidechainListener( + Source& source, + AccountID const& account, + std::weak_ptr&& federator, + Application& app, + beast::Journal j) + : InfoSub(source) + , ChainListener( + ChainListener::IsMainchain::no, + account, + std::move(federator), + j) + , app_(app) +{ +} + +void +SidechainListener::init(NetworkOPs& netOPs) +{ + auto e = netOPs.subAccountHistory(shared_from_this(), doorAccount_); + if (e != rpcSUCCESS) + LogicError("Could not subscribe to side chain door account history."); +} + +void +SidechainListener::send(Json::Value const& msg, bool) +{ + processMessage(msg); +} + +void +SidechainListener::stopHistoricalTxns(NetworkOPs& netOPs) +{ + netOPs.unsubAccountHistory( + shared_from_this(), doorAccount_, /*history only*/ true); +} + +void +SidechainListener::send( + std::string const& cmd, + Json::Value const& params, + RpcCallback onResponse) +{ + std::weak_ptr selfWeak = shared_from_this(); + auto job = [cmd, params, onResponse, selfWeak](Job&) { + auto self = selfWeak.lock(); + if (!self) + return; + + JLOGV( + self->j_.trace(), + "Sidechain send", + jv("command", cmd), + jv("params", params)); + + Json::Value const request = [&] { + Json::Value r(params); + r[jss::method] = cmd; + r[jss::jsonrpc] = "2.0"; + r[jss::ripplerpc] = "2.0"; + return r; + }(); + Resource::Charge loadType = Resource::feeReferenceRPC; + Resource::Consumer c; + RPC::JsonContext context{ + {self->j_, + self->app_, + loadType, + self->app_.getOPs(), + self->app_.getLedgerMaster(), + c, + Role::ADMIN, + {}, + {}, + RPC::apiMaximumSupportedVersion}, + std::move(request)}; + + Json::Value jvResult; + RPC::doCommand(context, jvResult); + JLOG(self->j_.trace()) << "Sidechain response: " << jvResult; + if (self->app_.config().standalone()) + self->app_.getOPs().acceptLedger(); + onResponse(jvResult); + }; + app_.getJobQueue().addJob(jtRPC, "federator rpc", job); +} + +} // namespace sidechain + +} // namespace ripple diff --git a/src/ripple/app/sidechain/impl/SidechainListener.h b/src/ripple/app/sidechain/impl/SidechainListener.h new file mode 100644 index 0000000000..229a55b290 --- /dev/null +++ b/src/ripple/app/sidechain/impl/SidechainListener.h @@ -0,0 +1,74 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_IMPL_SIDECHAINLISTENER_H_INCLUDED +#define RIPPLE_SIDECHAIN_IMPL_SIDECHAINLISTENER_H_INCLUDED + +#include +#include +#include +#include + +#include + +namespace ripple { + +class NetworkOPs; +class Application; + +namespace sidechain { + +class Federator; + +class SidechainListener : public InfoSub, + public ChainListener, + public std::enable_shared_from_this +{ + Application& app_; + +public: + SidechainListener( + Source& source, + AccountID const& account, + std::weak_ptr&& federator, + Application& app, + beast::Journal j); + + void + init(NetworkOPs& netOPs); + + ~SidechainListener() = default; + + void + send(Json::Value const& msg, bool) override; + + void + stopHistoricalTxns(NetworkOPs& netOPs); + + void + send( + std::string const& cmd, + Json::Value const& params, + RpcCallback onResponse) override; +}; + +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/app/sidechain/impl/SignatureCollector.cpp b/src/ripple/app/sidechain/impl/SignatureCollector.cpp new file mode 100644 index 0000000000..b567f55590 --- /dev/null +++ b/src/ripple/app/sidechain/impl/SignatureCollector.cpp @@ -0,0 +1,325 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace sidechain { + +std::chrono::seconds messageExpire = std::chrono::minutes{10}; + +uint256 +computeMessageSuppression(MessageId const& mId, Slice const& signature) +{ + Serializer s(128); + s.addBitString(mId); + s.addVL(signature); + return s.getSHA512Half(); +} + +SignatureCollector::SignatureCollector( + bool isMainChain, + SecretKey const& mySecKey, + PublicKey const& myPubKey, + beast::abstract_clock& c, + SignerList& signers, + Federator& federator, + Application& app, + beast::Journal j) + : isMainChain_(isMainChain) + , mySecKey_(mySecKey) + , myPubKey_(myPubKey) + , messages_(c) + , signers_(signers) + , federator_(federator) + , app_(app) + , j_(j) +{ +} + +void +SignatureCollector::signAndSubmit(Json::Value const& txJson) +{ + auto job = [tx = txJson, + myPK = myPubKey_, + mySK = mySecKey_, + chain = + isMainChain_ ? Federator::mainChain : Federator::sideChain, + f = federator_.weak_from_this(), + j = j_](Job&) mutable { + auto federator = f.lock(); + if (!federator) + return; + + STParsedJSONObject parsed(std::string(jss::tx_json), tx); + if (parsed.object == std::nullopt) + { + JLOGV(j.fatal(), "cannot parse transaction", jv("tx", tx)); + assert(0); + return; + } + try + { + parsed.object->setFieldVL(sfSigningPubKey, Slice(nullptr, 0)); + STTx tx(std::move(parsed.object.value())); + + MessageId mId{tx.getSigningHash()}; + Buffer sig{tx.getMultiSignature(calcAccountID(myPK), myPK, mySK)}; + federator->getSignatureCollector(chain).processSig( + mId, myPK, std::move(sig), std::move(tx)); + } + catch (...) + { + JLOGV(j.fatal(), "invalid transaction", jv("tx", tx)); + assert(0); + } + }; + + app_.getJobQueue().addJob(jtFEDERATORSIGNATURE, "federator signature", job); +} + +bool +SignatureCollector::processSig( + MessageId const& mId, + PublicKey const& pk, + Buffer const& sig, + std::optional const& txOpt) +{ + JLOGV( + j_.trace(), + "processSig", + jv("public key", strHex(pk)), + jv("message", mId)); + if (!signers_.isFederator(pk)) + { + return false; + } + + auto valid = addSig(mId, pk, sig, txOpt); + if (txOpt) + shareSig(mId, sig); + return valid; +} + +void +SignatureCollector::expire() +{ + std::lock_guard lock(mtx_); + beast::expire(messages_, messageExpire); +} + +bool +SignatureCollector::addSig( + MessageId const& mId, + PublicKey const& pk, + Buffer const& sig, + std::optional const& txOpt) +{ + JLOGV( + j_.trace(), + "addSig", + jv("message", mId), + jv("public key", strHex(pk)), + jv("sig", strHex(sig))); + + std::lock_guard lock(mtx_); + auto txi = messages_.find(mId); + if (txi == messages_.end()) + { + PeerSignatureMap sigMaps; + sigMaps.emplace(pk, sig); + MultiSigMessage m{sigMaps, txOpt}; + messages_.emplace(mId, std::move(m)); + return true; + } + + auto const verifySingle = [&](PublicKey const& pk, + Buffer const& sig) -> bool { + Serializer s; + s.add32(HashPrefix::txMultiSign); + (*txi->second.tx_).addWithoutSigningFields(s); + s.addBitString(calcAccountID(pk)); + return verify(pk, s.slice(), sig, true); + }; + + MultiSigMessage& message = txi->second; + if (txOpt) + { + message.tx_.emplace(std::move(*txOpt)); + + for (auto i = message.sigMaps_.begin(); i != message.sigMaps_.end();) + { + if (verifySingle(i->first, i->second)) + ++i; + else + { + JLOGV( + j_.trace(), + "verifySingle failed", + jv("public key", strHex(i->first))); + message.sigMaps_.erase(i); + } + } + } + else + { + if (message.tx_) + { + if (!verifySingle(pk, sig)) + { + JLOGV( + j_.trace(), + "verifySingle failed", + jv("public key", strHex(pk))); + return false; + } + } + } + message.sigMaps_.emplace(pk, sig); + + if (!message.submitted_ && message.tx_ && + message.sigMaps_.size() >= signers_.quorum()) + { + // message.submitted_ = true; + submit(mId, lock); + } + return true; +} + +void +SignatureCollector::shareSig(MessageId const& mId, Buffer const& sig) +{ + JLOGV(j_.trace(), "shareSig", jv("message", mId), jv("sig", strHex(sig))); + + std::shared_ptr toSend = [&]() -> std::shared_ptr { + protocol::TMFederatorAccountCtrlSignature m; + m.set_chain(isMainChain_ ? ::protocol::fct_MAIN : ::protocol::fct_SIDE); + m.set_publickey(myPubKey_.data(), myPubKey_.size()); + m.set_messageid(mId.data(), mId.size()); + m.set_signature(sig.data(), sig.size()); + + return std::make_shared( + m, protocol::mtFederatorAccountCtrlSignature); + }(); + + Overlay& overlay = app_.overlay(); + HashRouter& hashRouter = app_.getHashRouter(); + auto const suppression = computeMessageSuppression(mId, sig); + + overlay.foreach([&](std::shared_ptr const& p) { + hashRouter.addSuppressionPeer(suppression, p->id()); + JLOGV( + j_.trace(), + "sending signature to peer", + jv("pid", p->id()), + jv("mid", mId)); + p->send(toSend); + }); +} + +void +SignatureCollector::submit( + MessageId const& mId, + std::lock_guard const&) +{ + JLOGV(j_.trace(), "submit", jv("message", mId)); + + assert(messages_.find(mId) != messages_.end()); + auto& message = messages_[mId]; + assert(!message.submitted_); + message.submitted_ = true; + + STArray signatures; + auto sigCount = message.sigMaps_.size(); + assert(sigCount >= signers_.quorum()); + signatures.reserve(sigCount); + + for (auto const& item : message.sigMaps_) + { + STObject obj{sfSigner}; + obj[sfAccount] = calcAccountID(item.first); + obj[sfSigningPubKey] = item.first; + obj[sfTxnSignature] = item.second; + signatures.push_back(std::move(obj)); + }; + + std::sort( + signatures.begin(), + signatures.end(), + [](STObject const& lhs, STObject const& rhs) { + return lhs[sfAccount] < rhs[sfAccount]; + }); + + message.tx_->setFieldArray(sfSigners, std::move(signatures)); + + auto sp = message.tx_->getSeqProxy(); + if (sp.isTicket()) + { + Json::Value r; + r[jss::tx_blob] = strHex(message.tx_->getSerializer().peekData()); + + JLOGV(j_.trace(), "submit", jv("tx", r)); + auto callback = [&](Json::Value const& response) { + JLOGV( + j_.trace(), + "SignatureCollector::submit ", + jv("response", response)); + }; + rpcChannel_->send("submit", r, callback); + } + else + { + JLOGV( + j_.trace(), + "forward to federator to submit", + jv("tx", strHex(message.tx_->getSerializer().peekData()))); + federator_.addTxToSend( + (isMainChain_ ? Federator::ChainType::mainChain + : Federator::ChainType::sideChain), + sp.value(), + *(message.tx_)); + } +} + +void +SignatureCollector::setRpcChannel(std::shared_ptr channel) +{ + rpcChannel_ = std::move(channel); +} + +} // namespace sidechain +} // namespace ripple diff --git a/src/ripple/app/sidechain/impl/SignatureCollector.h b/src/ripple/app/sidechain/impl/SignatureCollector.h new file mode 100644 index 0000000000..ba7df10095 --- /dev/null +++ b/src/ripple/app/sidechain/impl/SignatureCollector.h @@ -0,0 +1,146 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_IMPL_SIGNATURE_COLLECTOR_H +#define RIPPLE_SIDECHAIN_IMPL_SIGNATURE_COLLECTOR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace Json { +class Value; +} + +namespace ripple { + +class Application; + +namespace sidechain { + +class SignerList; +class Federator; + +using PeerSignatureMap = hash_map; +using MessageId = uint256; +struct MultiSigMessage +{ + PeerSignatureMap sigMaps_; + std::optional tx_; + bool submitted_ = false; +}; + +class SignatureCollector +{ + std::shared_ptr rpcChannel_; + bool const isMainChain_; + SecretKey const mySecKey_; + PublicKey const myPubKey_; + mutable std::mutex mtx_; + beast::aged_unordered_map< + MessageId, + MultiSigMessage, + std::chrono::steady_clock, + beast::uhash<>> + GUARDED_BY(mtx_) messages_; + + SignerList& signers_; + Federator& federator_; + Application& app_; + beast::Journal j_; + +public: + SignatureCollector( + bool isMainChain, + SecretKey const& mySecKey, + PublicKey const& myPubKey, + beast::abstract_clock& c, + SignerList& signers, + Federator& federator, + Application& app, + beast::Journal j); + + /** + * sign the tx, and share with network + * once quorum signatures are collected, the tx will be submitted + * @param tx the transaction to be signed and later submitted + */ + void + signAndSubmit(Json::Value const& tx); + + /** + * verify the signature and remember it. + * If quorum signatures are collected for the same MessageId, + * a tx will be submitted. + * + * @param mId identify the tx + * @param pk public key of signer + * @param sig signature + * @param txOpt the transaction, only used by the local node + * @return if the signature is from a federator + */ + bool + processSig( + MessageId const& mId, + PublicKey const& pk, + Buffer const& sig, + std::optional const& txOpt); + + /** + * remove stale signatures + */ + void + expire() EXCLUDES(mtx_); // TODO retry logic + + void + setRpcChannel(std::shared_ptr channel); + +private: + // verify a signature (if it is from a peer) and add to a collection + bool + addSig( + MessageId const& mId, + PublicKey const& pk, + Buffer const& sig, + std::optional const& txOpt) EXCLUDES(mtx_); + + // share a signature to the network + void + shareSig(MessageId const& mId, Buffer const& sig); + // submit a tx since it collected quorum signatures + void + submit(MessageId const& mId, std::lock_guard const&) + REQUIRES(mtx_); +}; + +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/app/sidechain/impl/SignerList.cpp b/src/ripple/app/sidechain/impl/SignerList.cpp new file mode 100644 index 0000000000..5962a95cba --- /dev/null +++ b/src/ripple/app/sidechain/impl/SignerList.cpp @@ -0,0 +1,51 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 + +namespace ripple { +namespace sidechain { + +SignerList::SignerList( + AccountID const& account, + hash_set const& signers, + beast::Journal j) + : account_(account) + , signers_(signers) + , quorum_(static_cast(std::ceil(signers.size() * 0.8))) + , j_(j) +{ + (void)j_; +} + +bool +SignerList::isFederator(PublicKey const& pk) const +{ + std::lock_guard lock(mtx_); + return signers_.find(pk) != signers_.end(); +} + +std::uint32_t +SignerList::quorum() const +{ + std::lock_guard lock(mtx_); + return quorum_; +} +} // namespace sidechain +} // namespace ripple diff --git a/src/ripple/app/sidechain/impl/SignerList.h b/src/ripple/app/sidechain/impl/SignerList.h new file mode 100644 index 0000000000..ffd1e157a8 --- /dev/null +++ b/src/ripple/app/sidechain/impl/SignerList.h @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_IMPL_SIGNER_LIST_H +#define RIPPLE_SIDECHAIN_IMPL_SIGNER_LIST_H + +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { +namespace sidechain { + +/** + * grow to handle singer list changes + */ +class SignerList +{ + AccountID const account_; + mutable std::mutex mtx_; + hash_set GUARDED_BY(mtx_) signers_; + std::uint32_t GUARDED_BY(mtx_) quorum_; + beast::Journal j_; + +public: + SignerList( + AccountID const& account, + hash_set const& signers, + beast::Journal j); + ~SignerList() = default; + + bool + isFederator(PublicKey const& pk) const EXCLUDES(mtx_); + + std::uint32_t + quorum() const EXCLUDES(mtx_); +}; + +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/app/sidechain/impl/TicketHolder.cpp b/src/ripple/app/sidechain/impl/TicketHolder.cpp new file mode 100644 index 0000000000..d61b1b1b59 --- /dev/null +++ b/src/ripple/app/sidechain/impl/TicketHolder.cpp @@ -0,0 +1,791 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 +#include +#include +#include + +namespace ripple { +namespace sidechain { + +std::string +TicketPurposeToStr(TicketPurpose tp) +{ + switch (tp) + { + case TicketPurpose::mainDoorKeeper: + return "mainDoorKeeper"; + case TicketPurpose::sideDoorKeeper: + return "sideDoorKeeper"; + case TicketPurpose::updateSignerList: + return "updateSignerList"; + default: + break; + } + return "unknown"; +} + +TicketHolder::TicketHolder( + bool isMainChain, + AccountID const& account, + Federator& federator, + beast::Journal j) + : isMainChain_(isMainChain) + , accountStr_(toBase58(account)) + , federator_(federator) + , j_(j) +{ +} + +void +TicketHolder::init() +{ + std::lock_guard lock(mtx_); + if (initData_.status_ != InitializeStatus::waitLedger) + return; + initData_.status_ = InitializeStatus::waitAccountObject; + rpcAccountObject(); +} + +std::optional +TicketHolder::getTicket(TicketPurpose purpose, PeekOrTake pt) +{ + std::lock_guard lock(mtx_); + + if (initData_.status_ != InitializeStatus::initialized) + { + JLOGV( + j_.debug(), + "TicketHolder getTicket but ticket holder not initialized", + jv("chain", (isMainChain_ ? "main" : "side")), + jv("purpose", TicketPurposeToStr(purpose))); + + if (initData_.status_ == InitializeStatus::needToQueryTx) + rpcTx(lock); + return {}; + } + + auto index = static_cast>(purpose); + if (tickets_[index].status_ == AutoRenewedTicket::Status::available) + { + if (pt == PeekOrTake::take) + { + JLOGV( + j_.trace(), + "getTicket", + jv("chain", (isMainChain_ ? "main" : "side")), + jv("seq", tickets_[index].seq_)); + tickets_[index].status_ = AutoRenewedTicket::Status::taken; + } + return tickets_[index].seq_; + } + if (pt == PeekOrTake::take) + { + JLOGV( + j_.trace(), + "getTicket", + jv("chain", (isMainChain_ ? "main" : "side")), + jv("no ticket for ", TicketPurposeToStr(purpose))); + } + return {}; +} + +void +TicketHolder::onEvent(event::TicketCreateResult const& e) +{ + std::lock_guard lock(mtx_); + if (initData_.status_ != InitializeStatus::initialized) + { + JLOG(j_.trace()) << "TicketHolder queues an event"; + initData_.toReplay_.emplace(e); + return; + } + processEvent(e, lock); +} + +void +TicketHolder::onEvent(event::BootstrapTicket const& e) +{ + std::lock_guard lock(mtx_); + if (initData_.status_ != InitializeStatus::initialized) + { + JLOG(j_.trace()) << "TicketHolder queues an event"; + initData_.bootstrapTicketToReplay_.emplace(e); + return; + } + processEvent(e, lock); +} + +Json::Value +TicketHolder::getInfo() const +{ + Json::Value ret{Json::objectValue}; + { + std::lock_guard lock{mtx_}; + if (initData_.status_ == InitializeStatus::initialized) + { + ret["initialized"] = "true"; + Json::Value tickets{Json::arrayValue}; + for (auto const& t : tickets_) + { + Json::Value tj{Json::objectValue}; + tj["ticket_seq"] = t.seq_; + tj["status"] = t.status_ == AutoRenewedTicket::Status::taken + ? "taken" + : "available"; + tickets.append(tj); + } + ret["tickets"] = tickets; + } + else + { + ret["initialized"] = "false"; + } + } + return ret; +} + +void +TicketHolder::rpcAccountObject() +{ + Json::Value params = [&] { + Json::Value r; + r[jss::account] = accountStr_; + r[jss::ledger_index] = "validated"; + r[jss::type] = "ticket"; + r[jss::limit] = 250; + return r; + }(); + + rpcChannel_->send( + "account_objects", + params, + [isMainChain = isMainChain_, + f = federator_.weak_from_this()](Json::Value const& response) { + auto federator = f.lock(); + if (!federator) + return; + federator->getTicketRunner().accountObjectResult( + isMainChain, response); + }); +} + +void +TicketHolder::accountObjectResult(Json::Value const& rpcResult) +{ + auto ledgerNAccountObjectOpt = + [&]() -> std::optional> { + try + { + if (rpcResult.isMember(jss::error)) + { + return {}; + } + if (!rpcResult[jss::validated].asBool()) + { + return {}; + } + if (rpcResult[jss::account] != accountStr_) + { + return {}; + } + if (!rpcResult[jss::ledger_index].isIntegral()) + { + return {}; + } + if (!rpcResult.isMember(jss::account_objects) || + !rpcResult[jss::account_objects].isArray()) + { + return {}; + } + return std::make_pair( + rpcResult[jss::ledger_index].asUInt(), + rpcResult[jss::account_objects]); + } + catch (...) + { + return {}; + } + }(); + + if (!ledgerNAccountObjectOpt) + { + // can reach here? + // should not since we only ask account_object after a validated ledger + JLOGV(j_.error(), "AccountObject", jv("result", rpcResult)); + assert(false); + return; + } + + auto& [ledgerIndex, accountObjects] = *ledgerNAccountObjectOpt; + std::lock_guard lock(mtx_); + if (initData_.status_ != InitializeStatus::waitAccountObject) + { + JLOG(j_.warn()) << "unexpected AccountObject"; + return; + } + + initData_.ledgerIndex_ = ledgerIndex; + for (auto const& o : accountObjects) + { + if (!o.isMember("LedgerEntryType") || + o["LedgerEntryType"] != jss::Ticket) + continue; + // the following fields are mandatory + uint256 txHash; + if (!txHash.parseHex(o["PreviousTxnID"].asString())) + { + JLOGV( + j_.error(), + "AccountObject cannot parse tx hash", + jv("result", rpcResult)); + assert(false); + return; + } + std::uint32_t ticketSeq = o["TicketSequence"].asUInt(); + if (initData_.tickets_.find(txHash) != initData_.tickets_.end()) + { + JLOGV( + j_.error(), + "AccountObject cannot parse tx hash", + jv("result", rpcResult)); + assert(false); + return; + } + initData_.tickets_.emplace(txHash, ticketSeq); + JLOGV( + j_.trace(), + "AccountObject, add", + jv("tx hash", txHash), + jv("ticketSeq", ticketSeq)); + } + + if (initData_.tickets_.empty()) + { + JLOG(j_.debug()) << "Door account has no tickets in current ledger, " + "unlikely but could happen"; + replay(lock); + } + else + { + rpcTx(lock); + } +} + +void +TicketHolder::rpcTx(std::lock_guard const&) +{ + assert(!initData_.tickets_.empty()); + initData_.status_ = InitializeStatus::waitTx; + for (auto const& t : initData_.tickets_) + { + JLOG(j_.trace()) << "TicketHolder query tx " << t.first; + Json::Value params; + params[jss::transaction] = strHex(t.first); + rpcChannel_->send( + "tx", + params, + [isMainChain = isMainChain_, + f = federator_.weak_from_this()](Json::Value const& response) { + auto federator = f.lock(); + if (!federator) + return; + federator->getTicketRunner().txResult(isMainChain, response); + }); + } +} + +void +TicketHolder::txResult(Json::Value const& rpcResult) +{ + std::lock_guard lock(mtx_); + if (initData_.status_ != InitializeStatus::waitTx && + initData_.status_ != InitializeStatus::needToQueryTx) + return; + auto txOpt = [&]() -> std::optional> { + try + { + if (rpcResult.isMember(jss::error)) + { + return {}; + } + if (rpcResult[jss::Account] != accountStr_) + { + return {}; + } + + if (rpcResult[jss::TransactionType] != "TicketCreate") + { + return {}; + } + + if (!rpcResult["SourceTag"].isIntegral()) + { + return {}; + } + std::uint32_t tp = rpcResult["SourceTag"].asUInt(); + if (tp >= static_cast>( + TicketPurpose::TP_NumberOfItems)) + { + return {}; + } + + uint256 txHash; + if (!txHash.parseHex(rpcResult[jss::hash].asString())) + { + return {}; + } + return std::make_pair(static_cast(tp), txHash); + } + catch (...) + { + return {}; + } + }(); + + if (!txOpt) + { + JLOGV( + j_.warn(), + "TicketCreate can not be found or has wrong format", + jv("result", rpcResult)); + if (initData_.status_ == InitializeStatus::waitTx) + initData_.status_ = InitializeStatus::needToQueryTx; + return; + } + + auto [tPurpose, txHash] = *txOpt; + if (initData_.tickets_.find(txHash) == initData_.tickets_.end()) + { + JLOGV( + j_.debug(), + "Repeated TicketCreate tx result", + jv("result", rpcResult)); + return; + } + + auto& ticket = initData_.tickets_[txHash]; + JLOGV( + j_.trace(), + "TicketHolder txResult", + jv("purpose", TicketPurposeToStr(tPurpose)), + jv("txHash", txHash)); + + auto index = static_cast>(tPurpose); + tickets_[index].seq_ = ticket; + tickets_[index].status_ = AutoRenewedTicket::Status::available; + initData_.tickets_.erase(txHash); + + if (initData_.tickets_.empty()) + { + replay(lock); + } +} + +void +TicketHolder::replay(std::lock_guard const& lock) +{ + assert(initData_.tickets_.empty()); + // replay bootstrap tickets first if any + while (!initData_.bootstrapTicketToReplay_.empty()) + { + auto e = initData_.bootstrapTicketToReplay_.front(); + processEvent(e, lock); + initData_.bootstrapTicketToReplay_.pop(); + } + + while (!initData_.toReplay_.empty()) + { + auto e = initData_.toReplay_.front(); + processEvent(e, lock); + initData_.toReplay_.pop(); + } + initData_.status_ = InitializeStatus::initialized; + JLOG(j_.info()) << "TicketHolder initialized"; +} + +template +void +TicketHolder::processEvent(E const& e, std::lock_guard const&) +{ + std::uint32_t const tSeq = e.txnSeq_ + 1; + if (e.sourceTag_ >= static_cast>( + TicketPurpose::TP_NumberOfItems)) + { + JLOGV( + j_.error(), + "Wrong sourceTag", + jv("chain", (isMainChain_ ? "main" : "side")), + jv("sourceTag", e.sourceTag_)); + assert(false); + return; + } + + auto purposeStr = + TicketPurposeToStr(static_cast(e.sourceTag_)); + + if (e.ledgerIndex_ <= initData_.ledgerIndex_) + { + JLOGV( + j_.trace(), + "TicketHolder, ignoring an old ticket", + jv("chain", (isMainChain_ ? "main" : "side")), + jv("ticket seq", tSeq), + jv("purpose", purposeStr)); + return; + } + + if (!e.success_) + { + JLOGV( + j_.error(), + "CreateTicket failed", + jv("chain", (isMainChain_ ? "main" : "side")), + jv("ticket seq", tSeq), + jv("purpose", purposeStr)); + assert(false); + return; + } + + JLOGV( + j_.trace(), + "TicketHolder, got a ticket", + jv("chain", (isMainChain_ ? "main" : "side")), + jv("ticket seq", tSeq), + jv("purpose", purposeStr)); + + std::uint32_t const ticketPurposeToIndex = e.sourceTag_; + + if (e.eventType() == event::EventType::bootstrap && + tickets_[ticketPurposeToIndex].seq_ != 0) + { + JLOGV( + j_.error(), + "Got a bootstrap ticket too late", + jv("chain", (isMainChain_ ? "main" : "side")), + jv("ticket seq", tSeq), + jv("purpose", purposeStr)); + assert(false); + return; + } + tickets_[ticketPurposeToIndex].seq_ = tSeq; + tickets_[ticketPurposeToIndex].status_ = + AutoRenewedTicket::Status::available; +} + +void +TicketHolder::setRpcChannel(std::shared_ptr channel) +{ + rpcChannel_ = std::move(channel); +} + +TicketRunner::TicketRunner( + const AccountID& mainAccount, + const AccountID& sideAccount, + Federator& federator, + beast::Journal j) + : mainAccountStr_(toBase58(mainAccount)) + , sideAccountStr_(toBase58(sideAccount)) + , federator_(federator) + , mainHolder_(true, mainAccount, federator, j) + , sideHolder_(false, sideAccount, federator, j) + , j_(j) +{ +} + +void +TicketRunner::setRpcChannel( + bool isMainChain, + std::shared_ptr channel) +{ + if (isMainChain) + mainHolder_.setRpcChannel(std::move(channel)); + else + sideHolder_.setRpcChannel(std::move(channel)); +} + +void +TicketRunner::init(bool isMainChain) +{ + if (isMainChain) + mainHolder_.init(); + else + sideHolder_.init(); +} + +void +TicketRunner::accountObjectResult( + bool isMainChain, + Json::Value const& rpcResult) +{ + if (isMainChain) + mainHolder_.accountObjectResult(rpcResult); + else + sideHolder_.accountObjectResult(rpcResult); +} + +void +TicketRunner::txResult(bool isMainChain, Json::Value const& rpcResult) +{ + if (isMainChain) + mainHolder_.txResult(rpcResult); + else + sideHolder_.txResult(rpcResult); +} + +bool +TicketRunner::trigger( + TicketPurpose purpose, + std::optional const& mainTxJson, + std::optional const& sideTxJson) +{ + if (!mainTxJson && !sideTxJson) + { + assert(false); + return false; + } + + auto ticketPair = + [&]() -> std::optional> { + std::lock_guard lock(mtx_); + if (!mainHolder_.getTicket(purpose, TicketHolder::PeekOrTake::peek) || + !sideHolder_.getTicket(purpose, TicketHolder::PeekOrTake::peek)) + { + JLOG(j_.trace()) << "TicketRunner tickets no ready"; + return {}; + } + auto mainTicket = + mainHolder_.getTicket(purpose, TicketHolder::PeekOrTake::take); + auto sideTicket = + sideHolder_.getTicket(purpose, TicketHolder::PeekOrTake::take); + assert(mainTicket && sideTicket); + JLOGV( + j_.trace(), + "TicketRunner trigger", + jv("main ticket", *mainTicket), + jv("side ticket", *sideTicket), + jv("purpose", TicketPurposeToStr(purpose))); + return {{*mainTicket, *sideTicket}}; + }(); + + if (!ticketPair) + return false; + + auto sendTriggerTx = [&](std::string const& accountStr, + std::uint32_t ticketSequence, + std::optional const& memoJson, + SignatureCollector& signatureCollector) { + XRPAmount const fee{Federator::accountControlTxFee}; + Json::Value txJson; + txJson[jss::TransactionType] = "AccountSet"; + txJson[jss::Account] = accountStr; + txJson[jss::Sequence] = 0; + txJson[jss::Fee] = to_string(fee); + txJson["SourceTag"] = + static_cast>(purpose); + txJson["TicketSequence"] = ticketSequence; + if (memoJson) + { + Serializer s; + try + { + STParsedJSONObject parsed(std::string(jss::tx_json), *memoJson); + if (!parsed.object) + { + JLOGV( + j_.fatal(), "invalid transaction", jv("tx", *memoJson)); + assert(0); + return; + } + parsed.object->setFieldVL(sfSigningPubKey, Slice(nullptr, 0)); + parsed.object->add(s); + } + catch (...) + { + JLOGV(j_.fatal(), "invalid transaction", jv("tx", *memoJson)); + assert(0); + return; + } + + Json::Value memos{Json::arrayValue}; + Json::Value memo; + auto const dataStr = strHex(s.peekData()); + memo[jss::Memo][jss::MemoData] = dataStr; + memos.append(memo); + txJson[jss::Memos] = memos; + JLOGV( + j_.trace(), + "TicketRunner", + jv("tx", *memoJson), + jv("tx packed", dataStr), + jv("packed size", dataStr.length())); + assert( + memo[jss::Memo][jss::MemoData].asString().length() <= + event::MemoStringMax); + } + signatureCollector.signAndSubmit(txJson); + }; + + sendTriggerTx( + mainAccountStr_, + ticketPair->first, + mainTxJson, + federator_.getSignatureCollector(Federator::ChainType::mainChain)); + sendTriggerTx( + sideAccountStr_, + ticketPair->second, + sideTxJson, + federator_.getSignatureCollector(Federator::ChainType::sideChain)); + return true; +} + +void +TicketRunner::onEvent( + std::uint32_t accountSeq, + const event::TicketCreateTrigger& e) +{ + Json::Value txJson; + XRPAmount const fee{Federator::accountControlTxFee}; + txJson[jss::TransactionType] = "TicketCreate"; + txJson[jss::Account] = + e.dir_ == event::Dir::mainToSide ? sideAccountStr_ : mainAccountStr_; + txJson[jss::Sequence] = accountSeq; + txJson[jss::Fee] = to_string(fee); + txJson["TicketCount"] = 1; + txJson["SourceTag"] = e.sourceTag_; + { + Json::Value memos{Json::arrayValue}; + { + Json::Value memo; + memo[jss::Memo][jss::MemoData] = to_string(e.txnHash_); + memos.append(memo); + } + if (!e.memoStr_.empty()) + { + Json::Value memo; + memo[jss::Memo][jss::MemoData] = e.memoStr_; + memos.append(memo); + } + txJson[jss::Memos] = memos; + } + JLOGV( + j_.trace(), + "TicketRunner TicketTriggerDetected", + jv("chain", (e.dir_ == event::Dir::mainToSide ? "main" : "side")), + jv("seq", accountSeq), + jv("CreateTicket tx", txJson)); + + if (e.dir_ == event::Dir::mainToSide) + federator_.getSignatureCollector(Federator::ChainType::sideChain) + .signAndSubmit(txJson); + else + federator_.getSignatureCollector(Federator::ChainType::mainChain) + .signAndSubmit(txJson); +} + +void +TicketRunner::onEvent( + std::uint32_t accountSeq, + const event::TicketCreateResult& e) +{ + auto const [fromChain, toChain] = e.dir_ == event::Dir::mainToSide + ? std::make_pair(Federator::sideChain, Federator::mainChain) + : std::make_pair(Federator::mainChain, Federator::sideChain); + + auto ticketSeq = e.txnSeq_ + 1; + JLOGV( + j_.trace(), + "TicketRunner CreateTicketResult", + jv("chain", + (fromChain == Federator::ChainType::mainChain ? "main" : "side")), + jv("ticket seq", ticketSeq)); + + if (fromChain == Federator::ChainType::mainChain) + mainHolder_.onEvent(e); + else + sideHolder_.onEvent(e); + + federator_.addSeqToSkip(fromChain, ticketSeq); + + if (accountSeq) + { + assert(!e.memoStr_.empty()); + auto txData = strUnHex(e.memoStr_); + if (!txData || !txData->size()) + { + assert(false); + return; + } + SerialIter sitTrans(makeSlice(*txData)); + STTx tx(sitTrans); + tx.setFieldU32(sfSequence, accountSeq); + + auto txJson = tx.getJson(JsonOptions::none); + // trigger hash + Json::Value memos{Json::arrayValue}; + { + Json::Value memo; + memo[jss::Memo][jss::MemoData] = to_string(e.txnHash_); + memos.append(memo); + } + txJson[jss::Memos] = memos; + + JLOGV( + j_.trace(), + "TicketRunner AccountControlTrigger", + jv("chain", + (toChain == Federator::ChainType::mainChain ? "side" : "main")), + jv("tx with added memos", txJson.toStyledString())); + + federator_.getSignatureCollector(toChain).signAndSubmit(txJson); + } +} + +void +TicketRunner::onEvent(const event::BootstrapTicket& e) +{ + auto ticketSeq = e.txnSeq_ + 1; + JLOGV( + j_.trace(), + "TicketRunner BootstrapTicket", + jv("chain", (e.isMainchain_ ? "main" : "side")), + jv("ticket seq", ticketSeq)); + + if (e.isMainchain_) + mainHolder_.onEvent(e); + else + sideHolder_.onEvent(e); +} + +Json::Value +TicketRunner::getInfo(bool isMainchain) const +{ + if (isMainchain) + return mainHolder_.getInfo(); + else + return sideHolder_.getInfo(); +} + +} // namespace sidechain +} // namespace ripple diff --git a/src/ripple/app/sidechain/impl/TicketHolder.h b/src/ripple/app/sidechain/impl/TicketHolder.h new file mode 100644 index 0000000000..1d293b90ca --- /dev/null +++ b/src/ripple/app/sidechain/impl/TicketHolder.h @@ -0,0 +1,255 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_IMPL_TICKET_BOOTH_H +#define RIPPLE_SIDECHAIN_IMPL_TICKET_BOOTH_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace ripple { +namespace sidechain { + +class Federator; + +enum class TicketPurpose : std::uint32_t { + mainDoorKeeper, + sideDoorKeeper, + updateSignerList, + TP_NumberOfItems +}; +std::string +TicketPurposeToStr(TicketPurpose tp); + +struct AutoRenewedTicket +{ + enum class Status { available, taken }; + + std::uint32_t seq_; + Status status_; + + AutoRenewedTicket() : seq_(0), status_(Status::taken) + { + } +}; + +class TicketHolder +{ + enum class InitializeStatus { + waitLedger, + waitAccountObject, + waitTx, + needToQueryTx, + initialized + }; + + struct InitializeData + { + InitializeStatus status_ = InitializeStatus::waitLedger; + hash_map tickets_; + std::queue toReplay_; + std::queue bootstrapTicketToReplay_; + std::uint32_t ledgerIndex_ = 0; + }; + + std::shared_ptr rpcChannel_; + bool isMainChain_; + std::string const accountStr_; + AutoRenewedTicket + tickets_[static_cast>( + TicketPurpose::TP_NumberOfItems)]; + InitializeData initData_; + Federator& federator_; + beast::Journal j_; + mutable std::mutex mtx_; + +public: + TicketHolder( + bool isMainChain, + AccountID const& account, + Federator& federator, + beast::Journal j); + + /** + * start to initialize the ticketHolder by sending accountObject RPC + */ + void + init() EXCLUDES(mtx_); + /** + * process accountObject result and find the tickets. + * Initialization is not completed, because a ticket ledger object + * does not have information about its purpose. + * The purpose is in the TicketCreate tx what created the ticket. + * So the ticketHolder queries the TicketCreate tx for each ticket found. + * @param rpcResult accountObject result + */ + void + accountObjectResult(Json::Value const& rpcResult) EXCLUDES(mtx_); + /** + * process tx RPC result + * Initialization is completed once all TicketCreate txns are found, + * one for every ticket found in the previous initialization step. + * @param rpcResult tx result + */ + void + txResult(Json::Value const& rpcResult) EXCLUDES(mtx_); + + enum class PeekOrTake { peek, take }; + /** + * take or peek the ticket for a purpose + * @param purpose the ticket purpose + * @param pt take or peek + * @return the ticket if exist and not taken + */ + std::optional + getTicket(TicketPurpose purpose, PeekOrTake pt) EXCLUDES(mtx_); + + /** + * process a TicketCreateResult event, update the ticket number and status + * It queues the event if the ticketHolder is not yet initialized. + * + * @param e the TicketCreateResult event + */ + void + onEvent(event::TicketCreateResult const& e) EXCLUDES(mtx_); + /** + * process a ticket created during network bootstrap + * @param e the BootstrapTicket event + */ + void + onEvent(event::BootstrapTicket const& e) EXCLUDES(mtx_); + + Json::Value + getInfo() const EXCLUDES(mtx_); + + void + setRpcChannel(std::shared_ptr channel); + +private: + void + rpcAccountObject(); + + void + rpcTx(std::lock_guard const&) REQUIRES(mtx_); + + // replay accumulated events before finish initialization + void + replay(std::lock_guard const&) REQUIRES(mtx_); + template + void + processEvent(E const& e, std::lock_guard const&) REQUIRES(mtx_); +}; + +class TicketRunner +{ + std::string const mainAccountStr_; + std::string const sideAccountStr_; + Federator& federator_; + TicketHolder mainHolder_; + TicketHolder sideHolder_; + beast::Journal j_; + // Only one thread at a time can grab tickets + mutable std::mutex mtx_; + +public: + TicketRunner( + AccountID const& mainAccount, + AccountID const& sideAccount, + Federator& federator, + beast::Journal j); + + // set RpcChannel for a ticketHolder + void + setRpcChannel(bool isMainChain, std::shared_ptr channel); + // init a ticketHolder + void + init(bool isMainChain); + // pass a accountObject RPC result to a ticketHolder + void + accountObjectResult(bool isMainChain, Json::Value const& rpcResult); + // pass a tx RPC result to a ticketHolder + void + txResult(bool isMainChain, Json::Value const& rpcResult); + + /** + * Start to run a protocol that submit a federator account control tx + * to the network. + * + * Comparing to a normal tx submission that takes one step, a federator + * account control tx (such as depositAuth and signerListSet) takes 3 steps: + * 1. use a ticket to send a accountSet no-op tx as a trigger + * 2. create a new ticket + * 3. submit the account control tx + * + * @param ticketPurpose the purpose of ticket. The purpose describes + * the account control tx use case. + * @param mainTxJson account control tx for main chain + * @param sideTxJson account control tx for side chain + * @note mainTxJson and sideTxJson cannot both be empty + * @return if the protocol started + */ + [[nodiscard]] bool + trigger( + TicketPurpose ticketPurpose, + std::optional const& mainTxJson, + std::optional const& sideTxJson) EXCLUDES(mtx_); + + /** + * process a TicketCreateTrigger event, by submitting TicketCreate tx + * + * This event is generated when the accountSet no-op tx + * (as the protocol trigger) appears in the tx stream, + * i.e. sorted with regular XChain payments. + */ + void + onEvent(std::uint32_t accountSeq, event::TicketCreateTrigger const& e); + /** + * process a TicketCreateResult event, update the ticketHolder. + * + * This event is generated when the TicketCreate tx appears + * in the tx stream. + */ + void + onEvent(std::uint32_t accountSeq, event::TicketCreateResult const& e); + + /** + * process a ticket created during network bootstrap + */ + void + onEvent(event::BootstrapTicket const& e); + + Json::Value + getInfo(bool isMainchain) const; +}; + +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/app/sidechain/impl/WebsocketClient.cpp b/src/ripple/app/sidechain/impl/WebsocketClient.cpp new file mode 100644 index 0000000000..ff9fba257e --- /dev/null +++ b/src/ripple/app/sidechain/impl/WebsocketClient.cpp @@ -0,0 +1,175 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2016 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 +#include +#include +#include + +#include + +#include +#include +#include + +#include + +namespace ripple { +namespace sidechain { + +template +std::string +WebsocketClient::buffer_string(ConstBuffers const& b) +{ + using boost::asio::buffer; + using boost::asio::buffer_size; + std::string s; + s.resize(buffer_size(b)); + buffer_copy(buffer(&s[0], s.size()), b); + return s; +} + +void +WebsocketClient::cleanup() +{ + ios_.post(strand_.wrap([this] { + if (!peerClosed_) + { + { + std::lock_guard l{m_}; + ws_.async_close({}, strand_.wrap([&](error_code ec) { + stream_.cancel(ec); + + std::lock_guard l(shutdownM_); + isShutdown_ = true; + shutdownCv_.notify_one(); + })); + } + } + else + { + std::lock_guard l(shutdownM_); + isShutdown_ = true; + shutdownCv_.notify_one(); + } + })); +} + +void +WebsocketClient::shutdown() +{ + cleanup(); + std::unique_lock l{shutdownM_}; + shutdownCv_.wait(l, [this] { return isShutdown_; }); +} + +WebsocketClient::WebsocketClient( + std::function callback, + boost::asio::io_service& ios, + boost::asio::ip::address const& ip, + std::uint16_t port, + std::unordered_map const& headers, + beast::Journal j) + : ios_(ios) + , strand_(ios_) + , stream_(ios_) + , ws_(stream_) + , callback_(callback) + , j_{j} +{ + try + { + boost::asio::ip::tcp::endpoint const ep{ip, port}; + stream_.connect(ep); + ws_.set_option(boost::beast::websocket::stream_base::decorator( + [&](boost::beast::websocket::request_type& req) { + for (auto const& h : headers) + req.set(h.first, h.second); + })); + ws_.handshake( + ep.address().to_string() + ":" + std::to_string(ep.port()), "/"); + ws_.async_read( + rb_, + strand_.wrap(std::bind( + &WebsocketClient::onReadMsg, this, std::placeholders::_1))); + } + catch (std::exception&) + { + cleanup(); + Rethrow(); + } +} + +WebsocketClient::~WebsocketClient() +{ + cleanup(); +} + +std::uint32_t +WebsocketClient::send(std::string const& cmd, Json::Value params) +{ + params[jss::method] = cmd; + params[jss::jsonrpc] = "2.0"; + params[jss::ripplerpc] = "2.0"; + + auto const id = nextId_++; + params[jss::id] = id; + auto const s = to_string(params); + + std::lock_guard l{m_}; + ws_.write_some(true, boost::asio::buffer(s)); + return id; +} + +void +WebsocketClient::onReadMsg(error_code const& ec) +{ + if (ec) + { + JLOGV(j_.trace(), "WebsocketClient::onReadMsg error", jv("ec", ec)); + if (ec == boost::beast::websocket::error::closed) + peerClosed_ = true; + return; + } + + Json::Value jv; + Json::Reader jr; + jr.parse(buffer_string(rb_.data()), jv); + rb_.consume(rb_.size()); + callback_(jv); + + std::lock_guard l{m_}; + ws_.async_read( + rb_, + strand_.wrap(std::bind( + &WebsocketClient::onReadMsg, this, std::placeholders::_1))); +} + +// Called when the read op terminates +void +WebsocketClient::onReadDone() +{ +} + +} // namespace sidechain +} // namespace ripple diff --git a/src/ripple/app/sidechain/impl/WebsocketClient.h b/src/ripple/app/sidechain/impl/WebsocketClient.h new file mode 100644 index 0000000000..99fec5d0c4 --- /dev/null +++ b/src/ripple/app/sidechain/impl/WebsocketClient.h @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_SIDECHAIN_IO_WEBSOCKET_CLIENT_H_INCLUDED +#define RIPPLE_SIDECHAIN_IO_WEBSOCKET_CLIENT_H_INCLUDED + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace ripple { +namespace sidechain { + +class WebsocketClient +{ + using error_code = boost::system::error_code; + + template + static std::string + buffer_string(ConstBuffers const& b); + + // mutex for ws_ + std::mutex m_; + + // mutex for shutdown + std::mutex shutdownM_; + bool isShutdown_ = false; + std::condition_variable shutdownCv_; + + boost::asio::io_service& ios_; + boost::asio::io_service::strand strand_; + boost::asio::ip::tcp::socket stream_; + boost::beast::websocket::stream GUARDED_BY( + m_) ws_; + boost::beast::multi_buffer rb_; + + std::atomic peerClosed_{false}; + + std::function callback_; + std::atomic nextId_{0}; + + beast::Journal j_; + + void + cleanup(); + +public: + // callback will be called from a io_service thread + WebsocketClient( + std::function callback, + boost::asio::io_service& ios, + boost::asio::ip::address const& ip, + std::uint16_t port, + std::unordered_map const& headers, + beast::Journal j); + + ~WebsocketClient(); + + // Returns command id that will be returned in the response + std::uint32_t + send(std::string const& cmd, Json::Value params) EXCLUDES(m_); + + void + shutdown() EXCLUDES(shutdownM_); + +private: + void + onReadMsg(error_code const& ec) EXCLUDES(m_); + + // Called when the read op terminates + void + onReadDone(); +}; + +} // namespace sidechain +} // namespace ripple + +#endif diff --git a/src/ripple/basics/ThreadSaftyAnalysis.h b/src/ripple/basics/ThreadSaftyAnalysis.h new file mode 100644 index 0000000000..b91b08a1c1 --- /dev/null +++ b/src/ripple/basics/ThreadSaftyAnalysis.h @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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. +*/ +//============================================================================== + +#ifndef RIPPLE_BASICS_THREAD_SAFTY_ANALYSIS_H_INCLUDED +#define RIPPLE_BASICS_THREAD_SAFTY_ANALYSIS_H_INCLUDED + +#ifdef RIPPLE_ENABLE_THREAD_SAFETY_ANNOTATIONS +#define THREAD_ANNOTATION_ATTRIBUTE__(x) __attribute__((x)) +#else +#define THREAD_ANNOTATION_ATTRIBUTE__(x) // no-op +#endif + +#define CAPABILITY(x) THREAD_ANNOTATION_ATTRIBUTE__(capability(x)) + +#define SCOPED_CAPABILITY THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable) + +#define GUARDED_BY(x) THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x)) + +#define PT_GUARDED_BY(x) THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x)) + +#define ACQUIRED_BEFORE(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__)) + +#define ACQUIRED_AFTER(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(acquired_after(__VA_ARGS__)) + +#define REQUIRES(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__)) + +#define REQUIRES_SHARED(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(requires_shared_capability(__VA_ARGS__)) + +#define ACQUIRE(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(acquire_capability(__VA_ARGS__)) + +#define ACQUIRE_SHARED(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(acquire_shared_capability(__VA_ARGS__)) + +#define RELEASE(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(release_capability(__VA_ARGS__)) + +#define RELEASE_SHARED(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(release_shared_capability(__VA_ARGS__)) + +#define RELEASE_GENERIC(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(release_generic_capability(__VA_ARGS__)) + +#define TRY_ACQUIRE(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_capability(__VA_ARGS__)) + +#define TRY_ACQUIRE_SHARED(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_shared_capability(__VA_ARGS__)) + +#define EXCLUDES(...) THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__)) + +#define ASSERT_CAPABILITY(x) THREAD_ANNOTATION_ATTRIBUTE__(assert_capability(x)) + +#define ASSERT_SHARED_CAPABILITY(x) \ + THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_capability(x)) + +#define RETURN_CAPABILITY(x) THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x)) + +#define NO_THREAD_SAFETY_ANALYSIS \ + THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis) + +#endif diff --git a/src/ripple/core/Job.h b/src/ripple/core/Job.h index 0f3bb718bb..b3a2409c98 100644 --- a/src/ripple/core/Job.h +++ b/src/ripple/core/Job.h @@ -60,20 +60,21 @@ enum JobType { jtREPLAY_TASK, // A Ledger replay task/subtask jtLEDGER_DATA, // Received data for a ledger we're acquiring jtTRANSACTION, // A transaction received from the network - jtMISSING_TXN, // Request missing transactions - jtREQUESTED_TXN, // Reply with requested transactions - jtBATCH, // Apply batched transactions - jtADVANCE, // Advance validated/acquired ledgers - jtPUBLEDGER, // Publish a fully-accepted ledger - jtTXN_DATA, // Fetch a proposed set - jtWAL, // Write-ahead logging - jtVALIDATION_t, // A validation from a trusted source - jtWRITE, // Write out hashed objects - jtACCEPT, // Accept a consensus ledger - jtPROPOSAL_t, // A proposal from a trusted source - jtNETOP_CLUSTER, // NetworkOPs cluster peer report - jtNETOP_TIMER, // NetworkOPs net timer processing - jtADMIN, // An administrative operation + jtFEDERATORSIGNATURE, // A signature from a sidechain federator + jtMISSING_TXN, // Request missing transactions + jtREQUESTED_TXN, // Reply with requested transactions + jtBATCH, // Apply batched transactions + jtADVANCE, // Advance validated/acquired ledgers + jtPUBLEDGER, // Publish a fully-accepted ledger + jtTXN_DATA, // Fetch a proposed set + jtWAL, // Write-ahead logging + jtVALIDATION_t, // A validation from a trusted source + jtWRITE, // Write out hashed objects + jtACCEPT, // Accept a consensus ledger + jtPROPOSAL_t, // A proposal from a trusted source + jtNETOP_CLUSTER, // NetworkOPs cluster peer report + jtNETOP_TIMER, // NetworkOPs net timer processing + jtADMIN, // An administrative operation // Special job types which are not dispatched by the job pool jtPEER, diff --git a/src/ripple/core/JobTypes.h b/src/ripple/core/JobTypes.h index 75ec5ec0b4..dd4c0fe6b6 100644 --- a/src/ripple/core/JobTypes.h +++ b/src/ripple/core/JobTypes.h @@ -90,6 +90,13 @@ private: add(jtUPDATE_PF, "updatePaths", 1, 0ms, 0ms); add(jtTRANSACTION, "transaction", maxLimit, 250ms, 1000ms); add(jtBATCH, "batch", maxLimit, 250ms, 1000ms); + // TODO chose ave latency and peak latency numbers + add(jtFEDERATORSIGNATURE, + "federatorSignature", + maxLimit, + false, + 250ms, + 1000ms); add(jtADVANCE, "advanceLedger", maxLimit, 0ms, 0ms); add(jtPUBLEDGER, "publishNewLedger", maxLimit, 3000ms, 4500ms); add(jtTXN_DATA, "fetchTxnData", 5, 0ms, 0ms); diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index a8d72eda24..edfb7156cd 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -1250,6 +1250,7 @@ public: {"deposit_authorized", &RPCParser::parseDepositAuthorized, 2, 3}, {"download_shard", &RPCParser::parseDownloadShard, 2, -1}, {"feature", &RPCParser::parseFeature, 0, 2}, + {"federator_info", &RPCParser::parseAsIs, 0, 0}, {"fetch_info", &RPCParser::parseFetchInfo, 0, 1}, {"gateway_balances", &RPCParser::parseGatewayBalances, 1, -1}, {"get_counts", &RPCParser::parseGetCounts, 0, 1}, diff --git a/src/ripple/overlay/impl/Message.cpp b/src/ripple/overlay/impl/Message.cpp index b4cb1f192a..f251efb5eb 100644 --- a/src/ripple/overlay/impl/Message.cpp +++ b/src/ripple/overlay/impl/Message.cpp @@ -87,6 +87,7 @@ Message::compress() case protocol::mtVALIDATORLISTCOLLECTION: case protocol::mtREPLAY_DELTA_RESPONSE: case protocol::mtTRANSACTIONS: + case protocol::mtFederatorXChainTxnSignature: return true; case protocol::mtPING: case protocol::mtCLUSTER: @@ -102,6 +103,7 @@ Message::compress() case protocol::mtGET_PEER_SHARD_INFO_V2: case protocol::mtPEER_SHARD_INFO_V2: case protocol::mtHAVE_TRANSACTIONS: + case protocol::mtFederatorAccountCtrlSignature: break; } return false; diff --git a/src/ripple/overlay/impl/PeerImp.cpp b/src/ripple/overlay/impl/PeerImp.cpp index 6f05328212..db82fe8d8d 100644 --- a/src/ripple/overlay/impl/PeerImp.cpp +++ b/src/ripple/overlay/impl/PeerImp.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,7 @@ #include #include +#include "protocol/SField.h" #include #include #include @@ -2928,6 +2930,339 @@ PeerImp::onMessage(std::shared_ptr const& m) << "onMessage: TMSquelch " << slice << " " << id() << " " << duration; } +void +PeerImp::onMessage( + std::shared_ptr const& m) +{ + std::shared_ptr federator = + app_.getSidechainFederator(); + + auto sidechainJ = app_.journal("SidechainFederator"); + + auto badData = [&](std::string msg) { + fee_ = Resource::feeBadData; + JLOG(p_journal_.warn()) << msg; + }; + + auto getTxnType = [](::protocol::TMFederatorTxnType tt) + -> std::optional { + switch (tt) + { + case ::protocol::TMFederatorTxnType::ftxnt_XCHAIN: + return sidechain::Federator::TxnType::xChain; + case ::protocol::TMFederatorTxnType::ftxnt_REFUND: + return sidechain::Federator::TxnType::refund; + default: + return {}; + } + }; + + auto getChainType = [](::protocol::TMFederatorChainType ct) + -> std::optional { + switch (ct) + { + case ::protocol::TMFederatorChainType::fct_SIDE: + return sidechain::Federator::sideChain; + case ::protocol::TMFederatorChainType::fct_MAIN: + return sidechain::Federator::mainChain; + default: + return {}; + } + }; + + auto getPublicKey = + [](std::string const& data) -> std::optional { + if (data.empty()) + return {}; + auto const s = makeSlice(data); + if (!publicKeyType(s)) + return {}; + return PublicKey(s); + }; + + auto getHash = [](std::string const& data) -> std::optional { + if (data.size() != 32) + return {}; + return uint256(data); + }; + + auto getAccountId = + [](std::string const& data) -> std::optional { + if (data.size() != 20) + return {}; + return AccountID(data); + }; + + auto const signingPK = getPublicKey(m->signingpk()); + if (!signingPK) + { + return badData("Invalid federator key"); + } + auto const txnType = getTxnType(m->txntype()); + if (!txnType) + { + return badData("Invalid txn type"); + } + auto const dstChain = getChainType(m->dstchain()); + if (!dstChain) + { + return badData("Invalid dst chain"); + } + auto const srcChainTxnHash = getHash(m->srcchaintxnhash()); + if (!srcChainTxnHash) + { + return badData("Invalid src chain txn hash"); + } + auto const dstChainTxnHash = getHash(m->dstchaintxnhash()); + if (*txnType == sidechain::Federator::TxnType::refund && !dstChainTxnHash) + { + return badData("Invalid dst chain txn hash for refund"); + } + if (*txnType == sidechain::Federator::TxnType::xChain && dstChainTxnHash) + { + return badData("Invalid dst chain txn hash for xchain"); + } + auto const srcChainSrcAccount = getAccountId(m->srcchainsrcaccount()); + if (!srcChainSrcAccount) + { + return badData("Invalid src chain src account"); + } + auto const dstChainSrcAccount = getAccountId(m->dstchainsrcaccount()); + if (!dstChainSrcAccount) + { + return badData("Invalid dst chain src account"); + } + auto const dstChainDstAccount = getAccountId(m->dstchaindstaccount()); + if (!dstChainDstAccount) + { + return badData("Invalid dst account"); + } + auto const seq = m->seq(); + auto const amt = [&m]() -> std::optional { + try + { + SerialIter iter{m->amount().data(), m->amount().size()}; + STAmount amt{iter, sfGeneric}; + if (!iter.empty() || amt.signum() <= 0) + { + return std::nullopt; + } + if (amt.native() && amt.xrp() > INITIAL_XRP) + { + return std::nullopt; + } + return amt; + } + catch (std::exception const&) + { + } + return std::nullopt; + }(); + + if (!amt) + { + return badData("Invalid amount"); + } + + Buffer sig{m->signature().data(), m->signature().size()}; + + JLOGV( + sidechainJ.trace(), + "Received signature from peer", + jv("id", id()), + jv("sig", strHex(sig.data(), sig.data() + sig.size()))); + + if (federator && federator->alreadySent(*dstChain, seq)) + { + // already sent this transaction, no need to forward signature + return; + } + + uint256 const suppression = sidechain::crossChainTxnSignatureId( + *signingPK, + *srcChainTxnHash, + dstChainTxnHash, + *amt, + *dstChainSrcAccount, + *dstChainDstAccount, + seq, + sig); + app_.getHashRouter().addSuppressionPeer(suppression, id_); + + app_.getJobQueue().addJob( + jtFEDERATORSIGNATURE, + "federator signature", + [self = shared_from_this(), + federator = std::move(federator), + suppression, + txnType, + dstChain, + signingPK, + srcChainTxnHash, + dstChainTxnHash, + amt, + srcChainSrcAccount, + dstChainDstAccount, + seq, + m, + j = sidechainJ, + sig = std::move(sig)](Job&) mutable { + auto& hashRouter = self->app_.getHashRouter(); + if (auto const toSkip = hashRouter.shouldRelay(suppression)) + { + auto const toSend = std::make_shared( + *m, protocol::mtFederatorXChainTxnSignature); + self->overlay_.foreach([&](std::shared_ptr const& p) { + hashRouter.addSuppressionPeer(suppression, p->id()); + if (toSkip->count(p->id())) + { + JLOGV( + j.trace(), + "Not forwarding signature to peer", + jv("id", p->id()), + jv("suppression", suppression)); + return; + } + JLOGV( + j.trace(), + "Forwarding signature to peer", + jv("id", p->id()), + jv("suppression", suppression)); + p->send(toSend); + }); + } + + if (federator) + { + // Signature is checked in `addPendingTxnSig` + federator->addPendingTxnSig( + *txnType, + *dstChain, + *signingPK, + *srcChainTxnHash, + dstChainTxnHash, + *amt, + *srcChainSrcAccount, + *dstChainDstAccount, + seq, + std::move(sig)); + } + }); +} + +void +PeerImp::onMessage( + std::shared_ptr const& m) +{ + std::shared_ptr federator = + app_.getSidechainFederator(); + + auto sidechainJ = app_.journal("SidechainFederator"); + + auto badData = [&](std::string msg) { + fee_ = Resource::feeBadData; + JLOG(p_journal_.warn()) << msg; + }; + auto getChainType = [](::protocol::TMFederatorChainType ct) + -> std::optional { + switch (ct) + { + case ::protocol::TMFederatorChainType::fct_SIDE: + return sidechain::Federator::sideChain; + case ::protocol::TMFederatorChainType::fct_MAIN: + return sidechain::Federator::mainChain; + default: + return {}; + } + }; + auto const dstChain = getChainType(m->chain()); + if (!dstChain) + { + return badData("Invalid dst chain"); + } + auto getPublicKey = + [](std::string const& data) -> std::optional { + if (data.empty()) + return {}; + auto const s = makeSlice(data); + if (!publicKeyType(s)) + return {}; + return PublicKey(s); + }; + + auto getHash = [](std::string const& data) -> std::optional { + if (data.size() != 32) + return {}; + return uint256(data); + }; + + auto const pk = getPublicKey(m->publickey()); + if (!pk) + { + return badData("Invalid federator key"); + } + auto const mId = getHash(m->messageid()); + if (!mId) + { + return badData("Invalid txn hash"); + } + + Buffer sig{m->signature().data(), m->signature().size()}; + + JLOGV( + sidechainJ.trace(), + "Received signature from peer", + jv("id", id()), + jv("sig", strHex(sig.data(), sig.data() + sig.size()))); + + uint256 const suppression = sidechain::computeMessageSuppression(*mId, sig); + app_.getHashRouter().addSuppressionPeer(suppression, id_); + + app_.getJobQueue().addJob( + jtFEDERATORSIGNATURE, + "federator signature", + [self = shared_from_this(), + federator = std::move(federator), + suppression, + pk, + mId, + m, + j = sidechainJ, + chain = *dstChain, + sig = std::move(sig)](Job&) mutable { + auto& hashRouter = self->app_.getHashRouter(); + if (auto const toSkip = hashRouter.shouldRelay(suppression)) + { + auto const toSend = std::make_shared( + *m, protocol::mtFederatorAccountCtrlSignature); + self->overlay_.foreach([&](std::shared_ptr const& p) { + hashRouter.addSuppressionPeer(suppression, p->id()); + if (toSkip->count(p->id())) + { + JLOGV( + j.trace(), + "Not forwarding signature to peer", + jv("id", p->id()), + jv("suppression", suppression)); + return; + } + JLOGV( + j.trace(), + "Forwarding signature to peer", + jv("id", p->id()), + jv("suppression", suppression)); + p->send(toSend); + }); + } + + if (federator) + { + // Signature is checked in `addPendingTxnSig` + federator->addPendingTxnSig(chain, *pk, *mId, std::move(sig)); + } + }); +} + //-------------------------------------------------------------------------- void diff --git a/src/ripple/overlay/impl/PeerImp.h b/src/ripple/overlay/impl/PeerImp.h index 8bed64e724..ba4bfbcc07 100644 --- a/src/ripple/overlay/impl/PeerImp.h +++ b/src/ripple/overlay/impl/PeerImp.h @@ -584,6 +584,12 @@ public: onMessage(std::shared_ptr const& m); void onMessage(std::shared_ptr const& m); + void + onMessage( + std::shared_ptr const& m); + void + onMessage( + std::shared_ptr const& m); private: //-------------------------------------------------------------------------- diff --git a/src/ripple/overlay/impl/ProtocolMessage.h b/src/ripple/overlay/impl/ProtocolMessage.h index 09861353a1..dcce0eabac 100644 --- a/src/ripple/overlay/impl/ProtocolMessage.h +++ b/src/ripple/overlay/impl/ProtocolMessage.h @@ -113,6 +113,10 @@ protocolMessageName(int type) return "get_peer_shard_info_v2"; case protocol::mtPEER_SHARD_INFO_V2: return "peer_shard_info_v2"; + case protocol::mtFederatorXChainTxnSignature: + return "federator_xchain_txn_signature"; + case protocol::mtFederatorAccountCtrlSignature: + return "federator_account_ctrl_signature"; default: break; } @@ -493,6 +497,14 @@ invokeProtocolMessage( success = detail::invoke( *header, buffers, handler); break; + case protocol::mtFederatorXChainTxnSignature: + success = detail::invoke( + *header, buffers, handler); + break; + case protocol::mtFederatorAccountCtrlSignature: + success = detail::invoke( + *header, buffers, handler); + break; default: handler.onMessageUnknown(header->message_type); success = true; diff --git a/src/ripple/overlay/impl/TrafficCount.cpp b/src/ripple/overlay/impl/TrafficCount.cpp index 9b35d47683..ead0eda85a 100644 --- a/src/ripple/overlay/impl/TrafficCount.cpp +++ b/src/ripple/overlay/impl/TrafficCount.cpp @@ -163,6 +163,9 @@ TrafficCount::categorize( if (type == protocol::mtTRANSACTIONS) return TrafficCount::category::requested_transactions; + if (type == protocol::mtFederatorXChainTxnSignature) + return TrafficCount::category::federator_xchain_txn_signature; + return TrafficCount::category::unknown; } diff --git a/src/ripple/overlay/impl/TrafficCount.h b/src/ripple/overlay/impl/TrafficCount.h index 9e212da791..03efe9d914 100644 --- a/src/ripple/overlay/impl/TrafficCount.h +++ b/src/ripple/overlay/impl/TrafficCount.h @@ -157,6 +157,8 @@ public: // TMTransactions requested_transactions, + federator_xchain_txn_signature, + unknown // must be last }; @@ -243,12 +245,13 @@ protected: {"getobject_share"}, // category::share_hash {"getobject_get"}, // category::get_hash {"proof_path_request"}, // category::proof_path_request - {"proof_path_response"}, // category::proof_path_response - {"replay_delta_request"}, // category::replay_delta_request - {"replay_delta_response"}, // category::replay_delta_response - {"have_transactions"}, // category::have_transactions - {"requested_transactions"}, // category::transactions - {"unknown"} // category::unknown + {"proof_path_response"}, // category::proof_path_response + {"replay_delta_request"}, // category::replay_delta_request + {"replay_delta_response"}, // category::replay_delta_response + {"have_transactions"}, // category::have_transactions + {"requested_transactions"}, // category::transactions + {"federator_xchain_txn_signature"}, // category::federator_xchain_txn_signature + {"unknown"} // category::unknown }}; }; diff --git a/src/ripple/proto/ripple.proto b/src/ripple/proto/ripple.proto index 74cbfe8f6c..ad66e34d34 100644 --- a/src/ripple/proto/ripple.proto +++ b/src/ripple/proto/ripple.proto @@ -33,6 +33,8 @@ enum MessageType mtPEER_SHARD_INFO_V2 = 62; mtHAVE_TRANSACTIONS = 63; mtTRANSACTIONS = 64; + mtFederatorXChainTxnSignature = 65; + mtFederatorAccountCtrlSignature = 66; } // token, iterations, target, challenge = issue demand for proof of work @@ -450,3 +452,42 @@ message TMHaveTransactions repeated bytes hashes = 1; } +enum TMFederatorChainType +{ + fct_SIDE = 1; + fct_MAIN = 2; +} + +enum TMFederatorTxnType +{ + ftxnt_XCHAIN = 1; // cross chain + ftxnt_REFUND = 2; +} + +message TMFederatorXChainTxnSignature +{ + required TMFederatorTxnType txnType = 1; + required TMFederatorChainType dstChain = 2; + required bytes signingPK = 3; // federator's signing public key (unencoded binary data) + // txn hash for the src chain (unencoded bigendian binary data) + // This will be origional transaction from the src account to the door account + required bytes srcChainTxnHash = 4; + // txn hash for the dst chain (unencoded bigendian binary data) + // This will be empty for XCHAIN transaction, and will the failed transaction from the + // door account to the dst account for refund txns. + optional bytes dstChainTxnHash = 5; + required bytes amount = 6; // STAmount in wire serialized format + required bytes srcChainSrcAccount = 7; // account id (unencoded bigendian binary data) + required bytes dstChainSrcAccount = 8; // account id (unencoded bigendian binary data) + required bytes dstChainDstAccount = 9; // account id (unencoded bigendian binary data) + required uint32 seq = 10; // sequence number + required bytes signature = 11; // (unencoded bigendian binary data) +} + +message TMFederatorAccountCtrlSignature +{ + required TMFederatorChainType chain = 1; + required bytes publicKey = 2; + required bytes messageId = 3; + required bytes signature = 4; +} diff --git a/src/ripple/protocol/STTx.h b/src/ripple/protocol/STTx.h index ca33abf8ac..da081a0c7c 100644 --- a/src/ripple/protocol/STTx.h +++ b/src/ripple/protocol/STTx.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_PROTOCOL_STTX_H_INCLUDED #define RIPPLE_PROTOCOL_STTX_H_INCLUDED +#include #include #include #include @@ -77,6 +78,20 @@ public: Blob getSignature() const; + Buffer + getSignature(PublicKey const& publicKey, SecretKey const& secretKey) const; + + // Get one of the multi-signatures + Buffer + getMultiSignature( + AccountID const& signingID, + PublicKey const& publicKey, + SecretKey const& secretKey) const; + + // unconditionally set signature. No error checking. + void + setSignature(Buffer const& sig); + uint256 getSigningHash() const; diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index c8e05c7421..a10b0e6123 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -160,6 +160,16 @@ getSigningData(STTx const& that) return s.getData(); } +static Blob +getMultiSigningData(STTx const& that, AccountID const& signingID) +{ + Serializer s; + s.add32(HashPrefix::txMultiSign); + that.addWithoutSigningFields(s); + s.addBitString(signingID); + return s.getData(); +} + uint256 STTx::getSigningHash() const { @@ -179,6 +189,30 @@ STTx::getSignature() const } } +Buffer +STTx::getSignature(PublicKey const& publicKey, SecretKey const& secretKey) const +{ + auto const data = getSigningData(*this); + return ripple::sign(publicKey, secretKey, makeSlice(data)); +} + +Buffer +STTx::getMultiSignature( + AccountID const& signingID, + PublicKey const& publicKey, + SecretKey const& secretKey) const +{ + auto const data = getMultiSigningData(*this, signingID); + return ripple::sign(publicKey, secretKey, makeSlice(data)); +} + +void +STTx::setSignature(Buffer const& sig) +{ + setFieldVL(sfTxnSignature, sig); + tid_ = getHash(HashPrefix::transactionID); +} + SeqProxy STTx::getSeqProxy() const { @@ -197,12 +231,7 @@ STTx::getSeqProxy() const void STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey) { - auto const data = getSigningData(*this); - - auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data)); - - setFieldVL(sfTxnSignature, sig); - tid_ = getHash(HashPrefix::transactionID); + setSignature(getSignature(publicKey, secretKey)); } Expected diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index a227af4298..39199aad97 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -70,6 +70,10 @@ JSS(Invalid); // JSS(LastLedgerSequence); // in: TransactionSign; field JSS(LedgerHashes); // ledger type. JSS(LimitAmount); // field. +JSS(Memo); // txn common field +JSS(Memos); // txn common field +JSS(MemoType); // txn common field +JSS(MemoData); // txn common field JSS(Offer); // ledger type. JSS(OfferCancel); // transaction type. JSS(OfferCreate); // transaction type. @@ -91,6 +95,10 @@ JSS(SetFlag); // field. JSS(SetRegularKey); // transaction type. JSS(SignerList); // ledger type. JSS(SignerListSet); // transaction type. +JSS(SignerEntry); // transaction type. +JSS(SignerEntries); // transaction type. +JSS(SignerQuorum); // transaction type. +JSS(SignerWeight); // transaction type. JSS(SigningPubKey); // field. JSS(TakerGets); // field. JSS(TakerPays); // field. @@ -312,6 +320,7 @@ JSS(last_close); // out: NetworkOPs JSS(last_refresh_time); // out: ValidatorSite JSS(last_refresh_status); // out: ValidatorSite JSS(last_refresh_message); // out: ValidatorSite +JSS(last_transaction_sent_seq); // out: federator_info JSS(ledger); // in: NetworkOPs, LedgerCleaner, // RPCHelpers // out: NetworkOPs, PeerImp @@ -339,6 +348,7 @@ JSS(limit); // in/out: AccountTx*, AccountOffers, JSS(limit_peer); // out: AccountLines JSS(lines); // out: AccountLines JSS(list); // out: ValidatorList +JSS(listener_info); // out: federator_info JSS(load); // out: NetworkOPs, PeerImp JSS(load_base); // out: NetworkOPs JSS(load_factor); // out: NetworkOPs @@ -355,6 +365,7 @@ JSS(local_txs); // out: GetCounts JSS(local_static_keys); // out: ValidatorList JSS(lowest_sequence); // out: AccountInfo JSS(lowest_ticket); // out: AccountInfo +JSS(mainchain); // out: federator_info JSS(majority); // out: RPC feature JSS(manifest); // out: ValidatorInfo, Manifest JSS(marker); // in/out: AccountTx, AccountOffers, @@ -436,6 +447,7 @@ JSS(peers); // out: InboundLedger, handlers/Peers, Overlay JSS(peer_disconnects); // Severed peer connection counter. JSS(peer_disconnects_resources); // Severed peer connections because of // excess resource consumption. +JSS(pending_transactions); // out: federator_info JSS(port); // in: Connect JSS(previous); // out: Reservations JSS(previous_ledger); // out: LedgerPropose @@ -508,7 +520,9 @@ JSS(server_version); // out: NetworkOPs JSS(settle_delay); // out: AccountChannels JSS(severity); // in: LogLevel JSS(shards); // in/out: GetCounts, DownloadShard +JSS(sidechain); // out: federator_info JSS(signature); // out: NetworkOPs, ChannelAuthorize +JSS(signatures); // out: federator_info JSS(signature_verified); // out: ChannelVerify JSS(signing_key); // out: NetworkOPs JSS(signing_keys); // out: ValidatorList @@ -536,6 +550,7 @@ JSS(sub_index); // in: LedgerEntry JSS(subcommand); // in: PathFind JSS(success); // rpc JSS(supported); // out: AmendmentTableImpl +JSS(sync_info); // out: federator_info JSS(system_time_offset); // out: NetworkOPs JSS(tag); // out: Peers JSS(taker); // in: Subscribe, BookOffers diff --git a/src/ripple/rpc/handlers/FederatorInfo.cpp b/src/ripple/rpc/handlers/FederatorInfo.cpp new file mode 100644 index 0000000000..2a81519c63 --- /dev/null +++ b/src/ripple/rpc/handlers/FederatorInfo.cpp @@ -0,0 +1,46 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 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 +#include +#include + +namespace ripple { + +Json::Value +doFederatorInfo(RPC::JsonContext& context) +{ + Json::Value ret{Json::objectValue}; + + if (auto f = context.app.getSidechainFederator()) + { + ret[jss::info] = f->getInfo(); + } + else + { + ret[jss::info] = "Not configured as a sidechain federator"; + } + + return ret; +} + +} // namespace ripple diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index 264d0a3f17..027466b0b4 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -59,6 +59,8 @@ doDownloadShard(RPC::JsonContext&); Json::Value doFeature(RPC::JsonContext&); Json::Value +doFederatorInfo(RPC::JsonContext&); +Json::Value doFee(RPC::JsonContext&); Json::Value doFetchInfo(RPC::JsonContext&); diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index 3613bf723e..fe3ca4a3c5 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -89,6 +89,7 @@ Handler const handlerArray[]{ #endif {"get_counts", byRef(&doGetCounts), Role::ADMIN, NO_CONDITION}, {"feature", byRef(&doFeature), Role::ADMIN, NO_CONDITION}, + {"federator_info", byRef(&doFederatorInfo), Role::USER, NO_CONDITION}, {"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER}, {"fetch_info", byRef(&doFetchInfo), Role::ADMIN, NO_CONDITION}, {"ledger_accept", diff --git a/src/ripple/rpc/impl/ServerHandlerImp.cpp b/src/ripple/rpc/impl/ServerHandlerImp.cpp index 3ac5f04a9d..e5f06f92bd 100644 --- a/src/ripple/rpc/impl/ServerHandlerImp.cpp +++ b/src/ripple/rpc/impl/ServerHandlerImp.cpp @@ -320,6 +320,7 @@ ServerHandlerImp::onWSMessage( if (size > RPC::Tuning::maxRequestSize || !Json::Reader{}.parse(jv, buffers) || !jv.isObject()) { + Json::Reader{}.parse(jv, buffers); // swd debug Json::Value jvResult(Json::objectValue); jvResult[jss::type] = jss::error; jvResult[jss::error] = "jsonInvalid";