From 27f0cae812fbd60142fd7efbc9b3383253ee7cb2 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Wed, 11 Sep 2013 11:37:03 -0700 Subject: [PATCH] Add PeerFinder peer discovery logic and unit test --- Builds/VisualStudio2012/RippleD.vcxproj | 11 +- .../VisualStudio2012/RippleD.vcxproj.filters | 19 +- src/BeastConfig.h | 8 + src/ripple_app/main/ripple_Application.cpp | 15 + src/ripple_app/main/ripple_Application.h | 2 + src/ripple_app/peers/ripple_Peer.cpp | 48 +++ .../peerfinder/ripple_PeerFinder.cpp | 369 ++++++++++++++++++ .../peerfinder/ripple_PeerFinder.h | 145 +++++++ src/ripple_core/ripple_core.cpp | 6 + src/ripple_core/ripple_core.h | 1 + src/ripple_data/protocol/ripple.proto | 9 + 11 files changed, 625 insertions(+), 8 deletions(-) create mode 100644 src/ripple_core/peerfinder/ripple_PeerFinder.cpp create mode 100644 src/ripple_core/peerfinder/ripple_PeerFinder.h diff --git a/Builds/VisualStudio2012/RippleD.vcxproj b/Builds/VisualStudio2012/RippleD.vcxproj index ec75a5638..fb722f3ab 100644 --- a/Builds/VisualStudio2012/RippleD.vcxproj +++ b/Builds/VisualStudio2012/RippleD.vcxproj @@ -784,13 +784,19 @@ true true - - + true true true true + + true + true + true + true + + true true @@ -1512,6 +1518,7 @@ + diff --git a/Builds/VisualStudio2012/RippleD.vcxproj.filters b/Builds/VisualStudio2012/RippleD.vcxproj.filters index dece3ea5b..8db08ba08 100644 --- a/Builds/VisualStudio2012/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2012/RippleD.vcxproj.filters @@ -154,6 +154,9 @@ {c69b07a2-44e5-4b06-99a9-81f5d137ea15} + + {d1648d3f-7d71-495d-afc9-576ed00d7185} + @@ -219,9 +222,6 @@ [1] Ripple\ripple_core\functional - - [1] Ripple\ripple_core - [1] Ripple\ripple_data\crypto @@ -840,9 +840,6 @@ [1] Ripple\ripple_core\validator - - [1] Ripple\ripple_core\test - [1] Ripple\ripple_core\validator @@ -876,6 +873,13 @@ [0] src\beast + + + [1] Ripple\ripple_core + + + [1] Ripple\ripple_core\peerfinder + @@ -1728,6 +1732,9 @@ [1] Ripple\ripple_core\validator + + [1] Ripple\ripple_core\peerfinder + diff --git a/src/BeastConfig.h b/src/BeastConfig.h index 37df5eda7..3b6a477a7 100644 --- a/src/BeastConfig.h +++ b/src/BeastConfig.h @@ -173,4 +173,12 @@ #define RIPPLE_APPLICATION_CLEAN_EXIT 0 #endif +// This is only here temporarily. Use it to turn off the sending of +// "ANNOUNCE" messages if you suspect that you're having problems +// because of it. + +#ifndef RIPPLE_USE_MT_ANNOUNCE +#define RIPPLE_USE_MT_ANNOUNCE 0 +#endif + #endif diff --git a/src/ripple_app/main/ripple_Application.cpp b/src/ripple_app/main/ripple_Application.cpp index 84ed7bea5..af2be2c9a 100644 --- a/src/ripple_app/main/ripple_Application.cpp +++ b/src/ripple_app/main/ripple_Application.cpp @@ -17,6 +17,7 @@ class ApplicationImp , public SharedSingleton , public NodeStore::Scheduler , LeakChecked + , PeerFinder::Callback { public: // RAII container for a boost::asio::io_service run by beast threads @@ -175,6 +176,7 @@ public: , mUNL (UniqueNodeList::New ()) , mProofOfWorkFactory (ProofOfWorkFactory::New ()) , m_loadManager (LoadManager::New ()) + , mPeerFinder (PeerFinder::New (*this)) // VFALCO End new stuff // VFALCO TODO replace all NULL with nullptr , mRpcDB (NULL) @@ -366,6 +368,11 @@ public: return *m_peers; } + PeerFinder& getPeerFinder () + { + return *mPeerFinder; + } + // VFALCO TODO Move these to the .cpp bool running () { @@ -698,6 +705,8 @@ private: void startNewLedger (); bool loadOldLedger (const std::string&, bool); + void onAnnounceAddress (); + private: Application::LockType mMasterLock; @@ -735,6 +744,7 @@ private: ScopedPointer m_peerProxyDoor; ScopedPointer m_wsPublicDoor; ScopedPointer m_wsPrivateDoor; + ScopedPointer mPeerFinder; // VFALCO End Clean stuff DatabaseCon* mRpcDB; @@ -1189,6 +1199,11 @@ void ApplicationImp::updateTables () } } +void ApplicationImp::onAnnounceAddress () +{ + // NIKB CODEME +} + //------------------------------------------------------------------------------ Application& getApp () diff --git a/src/ripple_app/main/ripple_Application.h b/src/ripple_app/main/ripple_Application.h index 1a9273934..bba0e3614 100644 --- a/src/ripple_app/main/ripple_Application.h +++ b/src/ripple_app/main/ripple_Application.h @@ -28,6 +28,7 @@ class SerializedLedgerEntry; class TransactionMaster; class TxQueue; class LocalCredentials; +class PeerFinder; class DatabaseCon; @@ -95,6 +96,7 @@ public: virtual TransactionMaster& getMasterTransaction () = 0; virtual TxQueue& getTxQueue () = 0; virtual LocalCredentials& getLocalCredentials () = 0; + virtual PeerFinder& getPeerFinder () = 0; virtual DatabaseCon* getRpcDB () = 0; virtual DatabaseCon* getTxnDB () = 0; diff --git a/src/ripple_app/peers/ripple_Peer.cpp b/src/ripple_app/peers/ripple_Peer.cpp index 46697f854..052d3e07c 100644 --- a/src/ripple_app/peers/ripple_Peer.cpp +++ b/src/ripple_app/peers/ripple_Peer.cpp @@ -335,6 +335,7 @@ private: void sendPacketForce (const PackedMessage::pointer & packet); void sendHello (); + void SendAnnounce (); void recvHello (protocol::TMHello & packet); void recvCluster (protocol::TMCluster & packet); @@ -345,6 +346,7 @@ private: void recvGetContacts (protocol::TMGetContacts & packet); void recvGetPeers (protocol::TMGetPeers & packet, Application::ScopedLockType& masterLockHolder); void recvPeers (protocol::TMPeers & packet); + void recvAnnounce (protocol::TMAnnounce & packet); void recvGetObjectByHash (const boost::shared_ptr& packet); void recvPing (protocol::TMPing & packet); void recvErrorMessage (protocol::TMErrorMsg & packet); @@ -834,6 +836,17 @@ void PeerImp::processReadBuffer () } break; + case protocol::mtANNOUNCE: + { + event->reName ("PeerImp::announce"); + protocol::TMAnnounce msg; + + if(msg.ParseFromArray (&mReadbuf[PackedMessage::kHeaderBytes], mReadbuf.size() - PackedMessage::kHeaderBytes)) + recvAnnounce (msg); + else + WriteLog (lsWARNING, Peer) << "parse error: " << type;; + } + case protocol::mtSEARCH_TRANSACTION: { event->reName ("PeerImp::searchtransaction"); @@ -1606,6 +1619,15 @@ void PeerImp::recvPeers (protocol::TMPeers& packet) } } +void PeerImp::recvAnnounce (protocol::TMAnnounce& packet) +{ + // NIKB TODO: First we need to push this announcement to peerfinder + // and then this is not a private peer, we need to "adjust" this + // announcement (i.e. the hop count) and push it out to all our + // other peers. We must be careful to avoid cycles (that is to + // never send an ANNOUNCE back to the peer we got it from). +} + void PeerImp::recvGetObjectByHash (const boost::shared_ptr& ptr) { protocol::TMGetObjectByHash& packet = *ptr; @@ -2362,6 +2384,32 @@ void PeerImp::sendHello () sendPacket (packet, true); } +void PeerImp::SendAnnounce () +{ + protocol::TMAnnounce a; + + std::string selfID = getApp().getLocalCredentials ().getNodePublic ().humanNodePublic (); + + a.set_serverid(selfID); + + // Since we are announcing ourselves, the hopcount is one and the "via" peer is us. + // If hopcount is anything else, the "via" peer must be different from the "serverid". + a.set_viapeerid(selfID); + a.set_hopcount(1); // A direct connection + + // Announce whether we're a private peer or not. Should we even send an ANNOUNCE + // for private peers? + a.set_privatepeer(getConfig ().PEER_PRIVATE); + + protocol::TMIPv4EndPoint *ep = a.add_connectpoints(); + + ep->set_ipv4(inet_addr (getNativeSocket ().local_endpoint ().address ().to_string ().c_str())); + ep->set_ipv4port(getConfig ().peerListeningPort); + + PackedMessage::pointer packet = boost::make_shared (a, protocol::mtANNOUNCE); + sendPacket (packet, true); +} + void PeerImp::sendGetPeers () { // Ask peer for known other peers. diff --git a/src/ripple_core/peerfinder/ripple_PeerFinder.cpp b/src/ripple_core/peerfinder/ripple_PeerFinder.cpp new file mode 100644 index 000000000..fe1fd32f6 --- /dev/null +++ b/src/ripple_core/peerfinder/ripple_PeerFinder.cpp @@ -0,0 +1,369 @@ +//------------------------------------------------------------------------------ +/* + Copyright (c) 2011-2013, OpenCoin, Inc. +*/ +//============================================================================== +/* + +PeerFinder +---------- + + Implements the logic for announcing and discovering IP addresses for + for connecting into the Ripple network. + +Introduction +------------ + +Each Peer (a computer running rippled) on the Ripple network requires a certain +number of connections to other peers. These connections form an "overlay +network." When a new peer wants to join the network, they need a robust source +of network addresses (IP adresses) in order to establish outgoing connections. +Once they have joined the network, they need a method of announcing their +availaibility of accepting incoming connections. + +The Ripple network, like all peer to peer networks, defines a "directed graph" +where each node represents a computer running the rippled software, and each +vertex indicates a network connection. The direction of the connection tells +us whether it is an outbound or inbound connection (from the perspective of +a particular node). + +Fact #1: + The total inbound and outbound connections of any overlay must be equal. + +This follows that for each node that has an established outbound connection, +there must exist another node that has received the corresponding inbound +connection. + +When a new peer joins the network it may or may not wish to receive inbound +connections. Some peers are unable to accept incoming connections for various. +For security reasons they may be behind a firewall that blocks accept requests. +The administers may decide they don't want the connection traffic. Or they +may wish to connect only to specific peers. Or they may simply be misconfigured. + +If a peer decides that it wishes to receive incoming connections, it needs +a method to announce its IP address and port number, the features that it +offers (for example, that it also services client requests), and the number +of available connection slots. This is to handle the case where the peer +reaches its desired number of peer connections, but may still want to inform +the network that it will service clients. It may also be desired to indicate +the number of free client slots. + +Pong +---- + +Once a peer is connected to the network we need a way both to inform our +neighbors of our status with respect to accepting connections, and also to +learn about new fresh addresses to connect to. For this we will define the "Pong" +message. + +"Connection Strategy" +--------------------- + +This is the overall strategy a peer uses to maintain its position in the Ripple +network graph + +We define these values: + + PeerCount (calculated) + The number of currently connected and established peers + + OutCount (calculated) + The number of peers in PeerCount that are outbound connections. + + MinOutCount (hard-coded constant) + The minimum number of OutCount we want. This also puts a floor + on PeerCount. This protects against sybil attacks and makes + sure that ledgers can get retrieved reliably. + 10 is the proposed value. + + MaxPeerCount (a constant set in the rippled.cfg) + The maximum number of peer connections, inbound or outbound, + that a peer wishes to maintain. Setting MaxPeerCount equal to + or below MinOutCount would disallow incoming connections. + + OutDesiredPercent (a baked-in program constant for now) + The peer's target value for OutCount. When the value of OutCount + is below this number, the peer will employ the Outgoing Strategy + to raise its value of OutCount. This value is initially a constant + in the program, defined by the developers. However, it + may be changed through the consensus process. + 15% is a proposed value. + +However, lets consider the case where OutDesired is exactly equal to MaxPeerCount / 2. +In this case, a stable state will be reached when every peer is full, and +has exactly the same number of inbound and outbound connections. The problem +here is that there are now no available incoming connection slots. No new +peers can enter the network. + +Lets consider the case where OutDesired is exactly equal to (MaxPeerCount / 2) - 1. +The stable state for this network (assuming all peers can accept incoming) will +leave us with network degree equal to MaxPeerCount - 2, with all peers having two +available incoming connection slots. The global number of incoming connection slots +will be equal to twice the number of nodes on the network. While this might seem to +be a desirable outcome, note that the connectedness (degree of the overlay) plays +a large part in determining the levels of traffic and ability to receive validations +from desired nodes. Having every node with available incoming connections also +means that entries in pong caches will continually fall out with new values and +information will become less useful. + +For this reason, we advise that the value of OutDesired be fractional. Upon startup, +a node will use its node ID (its 160 bit unique ID) to decide whether to round the +value of OutDesired up or down. Using this method, we can precisely control the +global number of available incoming connection slots. + +"Outgoing Strategy" +------------------- + +This is the method a peer uses to establish outgoing connections into the +Ripple network. + +A peer whose PeerCount is zero will use these steps: + 1. Attempt addresses from a local database of addresses + 2. Attempt addresses from a set of "well known" domains in rippled.cfg + + +This is the method used by a peer that is already connected to the Ripple network, +to adjust the number of outgoing connections it is maintaining. + + +"Incoming Strategy" +------------------------------ + +This is the method used by a peer to announce its ability and desire to receive +incoming connections both for the purpose of obtaining additional peer connections +and also for receiving requests from clients. + + + +Terms + +Overlay Network + http://en.wikipedia.org/wiki/Overlay_network + +Directed Graph + http://en.wikipedia.org/wiki/Directed_graph + +References: + +Gnutella 0.6 Protocol + 2.2.2 Ping (0x00) + 2.2.3 Pong (0x01) + 2.2.4 Use of Ping and Pong messages + 2.2.4.1 A simple pong caching scheme + 2.2.4.2 Other pong caching schemes + http://rfc-gnutella.sourceforge.net/src/rfc-0_6-draft.html + +Revised Gnutella Ping Pong Scheme + By Christopher Rohrs and Vincent Falco + http://rfc-gnutella.sourceforge.net/src/pong-caching.html + +*/ +//------------------------------------------------------------------------------ + +class PeerFinderImp + : public PeerFinder + , private ThreadWithCallQueue::EntryPoints + , private DeadlineTimer::Listener + , LeakChecked +{ +public: + // Tunable constants + enum + { + // How often our timer goes off to consult outside sources for IPs + secondsPerUpdate = 1 * 60 * 60, // once per hour + // How often we announce our IP + secondsPerBroadcast = 5 * 60, + + // The minimum number of peers we want + numberOfPeersMinimum = 4, + numberOfPeersMaximum = 10, + + // The minimum number of seconds a connection ought to be sustained + // before we consider it "stable" + secondsForStability = 60, // one minute + }; + + //-------------------------------------------------------------------------- + + /** The Logic for maintaining the list of Peer addresses. + We keep this in a separate class so it can be instantiated + for unit tests. + */ + class Logic + { + Callback &m_callback; + + public: + explicit Logic (Callback& callback) + : m_callback (callback) + { + } + + // Called on the PeerFinder thread + void onUpdateConnectionsStatus ( + Connections const& connections) + { + if (connections.numberTotal () < numberOfPeersMinimum) + { + // do something + } + else + { + // do something? + } + } + + void onPeerConnected ( + const PeerId& id) + { + + } + + void onPeerDisconnected ( + const PeerId& id) + { + + } + + void onAcceptTimer() + { + m_callback.onAnnounceAddress (); + } + }; + + //-------------------------------------------------------------------------- + +public: + explicit PeerFinderImp (Callback& callback) + : m_logic (callback) + , m_thread ("PeerFinder") + , m_acceptTimer (this) + , m_updateTimer (this) + { + m_thread.start (this); + } + + ~PeerFinderImp () + { + } + + void updateConnectionsStatus (Connections& connections) + { + // Queue the call to the logic + m_thread.call (&Logic::onUpdateConnectionsStatus, + &m_logic, connections); + } + + void onPeerConnected(const PeerId& id) + { + m_thread.call (&Logic::onPeerConnected, + &m_logic, id); + } + + void onPeerDisconnected(const PeerId& id) + { + m_thread.call (&Logic::onPeerDisconnected, + &m_logic, id); + } + + //-------------------------------------------------------------------------- + void onAcceptTimer () + { +#if 0 + static int x = 0; + + if(x == 0) + Debug::breakPoint (); + + x++; +#endif + } + + void onDeadlineTimer (DeadlineTimer& timer) + { + // This will make us fall into the idle proc as needed + // + if (timer == m_updateTimer) + m_thread.interrupt (); + else if (timer == m_acceptTimer) + m_thread.call (&Logic::onAcceptTimer, &m_logic); + } + + void threadInit () + { + m_updateTimer.setRecurringExpiration (secondsPerUpdate); + m_acceptTimer.setRecurringExpiration (secondsPerBroadcast); + } + + void threadExit () + { + } + + bool threadIdle () + { + bool interrupted = false; + + // This is where you can go into a loop and do stuff + // like process the lists, and what not. Just be + // sure to call: + // + // @code + // interrupted = interruptionPoint (); + // @encode + // + // From time to time. If it returns true then you + // need to exit this function so that Thread can + // process its asynchronous call queue and then come + // back into threadIdle() + + return interrupted; + } + +private: + Logic m_logic; + ThreadWithCallQueue m_thread; + DeadlineTimer m_acceptTimer; + DeadlineTimer m_updateTimer; +}; + +//------------------------------------------------------------------------------ + +PeerFinder* PeerFinder::New (PeerFinder::Callback& callback) +{ + return new PeerFinderImp (callback); +} + +//------------------------------------------------------------------------------ + +class PeerFinderTests : public UnitTest, + public PeerFinder::Callback +{ +public: + void testValidityChecks () + { + beginTestCase ("ip validation"); + + fail ("there's no code!"); + } + + void runTest () + { + PeerFinderImp::Logic logic (*this); + + beginTestCase ("logic"); + logic.onAcceptTimer (); + } + + void onAnnounceAddress () + { + + } + + PeerFinderTests () : UnitTest ("PeerFinder", "ripple", runManual) + { + } +}; + +static PeerFinderTests peerFinderTests; + diff --git a/src/ripple_core/peerfinder/ripple_PeerFinder.h b/src/ripple_core/peerfinder/ripple_PeerFinder.h new file mode 100644 index 000000000..7b9199c6b --- /dev/null +++ b/src/ripple_core/peerfinder/ripple_PeerFinder.h @@ -0,0 +1,145 @@ +//------------------------------------------------------------------------------ +/* + Copyright (c) 2011-2013, OpenCoin, Inc. +*/ +//============================================================================== + +#ifndef RIPPLE_PEERFINDER_H_INCLUDED +#define RIPPLE_PEERFINDER_H_INCLUDED + +/** The identifier we use to track peers in peerfinder +*/ +typedef uint160 PeerId; + + +/** Maintains a set of IP addresses used for getting into the network. +*/ +class PeerFinder : public Uncopyable +{ +public: + //-------------------------------------------------------------------------- + + /** Describes the state of our currently connected peers + */ + struct Connections + { + int numberIncoming; // number of inbound Peers + int numberOutgoing; // number of outbound Peers + + inline int numberTotal () const noexcept + { + return numberIncoming + numberOutgoing; + } + }; + + //-------------------------------------------------------------------------- + + /** An abstract address that can be turned into a socket endpoint. + */ + struct Address + { + virtual String asString () = 0; + }; + + /** An IPv4 address. + */ + struct AddressIPv4 : Address + { + AddressIPv4 (InputParser::IPv4Address const& address, uint16 port) + : m_address (address) + , m_port (port) + { + + } + + String asString () + { + return String () + + String (m_address.value [0]) + "." + + String (m_address.value [1]) + "." + + String (m_address.value [2]) + "." + + String (m_address.value [3]) + ":" + + String (m_port); + } + + private: + InputParser::IPv4Address m_address; + uint16 m_port; + }; + + //-------------------------------------------------------------------------- + + /** The Callback receives Peerfinder notifications. + The notifications are sent on a thread owned by the PeerFinder, + so it is best not to do too much work in here. Just post functor + to another worker thread or job queue and return. + */ + struct Callback + { + /** Announces our listening ip/port combinations to the network. + + @param address The address to broadcast. + */ + virtual void onAnnounceAddress () = 0; + + /** Indicates whether or not incoming connections should be accepted. + When we are full on incoming connections, future incoming + connections from valid peers should be politely turned away, + after giving them a random sample of other addresses to try + from our cache. + */ + //virtual void onSetAcceptStatus (bool shouldAcceptIncoming) = 0; + + /** Called periodically to update the callback's list of eligible addresses. + This is used for making new outgoing connections, for + handing out addresses to peers, and for periodically seeding the + network wth hop-limited broadcasts of IP addresses. + */ + //virtual void onNewAddressesAvailable (std::vector
const& list) = 0; + }; + + //-------------------------------------------------------------------------- + + /** Create a new PeerFinder object. + */ + static PeerFinder* New (Callback& callback); + + /** Destroy the object. + + Any pending source fetch operations are aborted. + + There may be some listener calls made before the + destructor returns. + */ + virtual ~PeerFinder () { } + + /** Inform the PeerFinder of the status of our connections. + + This call queues an asynchronous operation to the PeerFinder's thread + and returns immediately. Normally this is called by the Peer code + when the counts change. + + Thread-safety: + Safe to call from any thread + + @see Peer + */ + virtual void updateConnectionsStatus (Connections& connections) = 0; + + + /** Called when a new peer connection is established. + Internally, we add the peer to our tracking table, validate that + we can connect to it, and begin advertising it to others after + we are sure that its connection is stable. + */ + virtual void onPeerConnected(const PeerId& id) = 0; + + /** Called when an existing peer connection drops for whatever reason. + Internally, we mark the peer as no longer connected, calculate + stability metrics, and consider whether we should try to reconnect + to it or drop it from our list. + */ + virtual void onPeerDisconnected(const PeerId& id) = 0; +}; + +#endif diff --git a/src/ripple_core/ripple_core.cpp b/src/ripple_core/ripple_core.cpp index 73ae13755..0d0d421fe 100644 --- a/src/ripple_core/ripple_core.cpp +++ b/src/ripple_core/ripple_core.cpp @@ -8,6 +8,9 @@ #include "ripple_core.h" +// Needed for InputParser +#include "beast/modules/beast_asio/beast_asio.h" + #include #include "beast/modules/beast_core/system/BeforeBoost.h" // must come first @@ -71,4 +74,7 @@ namespace ripple #include "validator/ValidatorSourceTrustedURL.cpp" #include "validator/Validators.cpp" + #include "peerfinder/ripple_PeerFinder.h" // private (for now) +#include "peerfinder/ripple_PeerFinder.cpp" + } diff --git a/src/ripple_core/ripple_core.h b/src/ripple_core/ripple_core.h index 2defc8b5d..5d0d0d1ea 100644 --- a/src/ripple_core/ripple_core.h +++ b/src/ripple_core/ripple_core.h @@ -38,6 +38,7 @@ namespace ripple #include "node/NodeObject.h" #include "node/NodeStore.h" +#include "peerfinder/ripple_PeerFinder.h" #include "validator/Validators.h" } diff --git a/src/ripple_data/protocol/ripple.proto b/src/ripple_data/protocol/ripple.proto index 3352eb4f5..7a59725f6 100644 --- a/src/ripple_data/protocol/ripple.proto +++ b/src/ripple_data/protocol/ripple.proto @@ -14,6 +14,7 @@ enum MessageType mtCONTACT = 11; mtGET_PEERS = 12; mtPEERS = 13; + mtANNOUNCE = 14; // operations for 'small' nodes mtSEARCH_TRANSACTION = 20; @@ -228,6 +229,14 @@ message TMPeers repeated TMIPv4EndPoint nodes = 1; } +message TMAnnounce +{ + required bytes serverID = 1; // The ID of this server + required bool privatePeer = 2; // This peer is private - the announce will not be propagated + required uint32 hopCount = 3; // The hop count of this server + required bytes viaPeerID = 4; // The ID of the peer through which we know this server + repeated TMIPv4EndPoint connectPoints = 5; // Addresses on which this server accepts connections (can be empty) +}; message TMSearchTransaction {