xrpld
Loading...
Searching...
No Matches
DisputedTx.h
1#pragma once
2
3#include <xrpld/consensus/ConsensusParms.h>
4
5#include <xrpl/basics/Log.h>
6#include <xrpl/beast/utility/Journal.h>
7#include <xrpl/json/json_writer.h>
8
9#include <boost/container/flat_map.hpp>
10
11#include <utility>
12
13namespace xrpl {
14
28
29template <class Tx, class NodeId>
31{
32 using TxID_t = Tx::ID;
33 using Map_t = boost::container::flat_map<NodeId, bool>;
34
35public:
43 DisputedTx(Tx tx, bool ourVote, std::size_t numPeers, beast::Journal j)
44 : ourVote_(ourVote), tx_(std::move(tx)), j_(j)
45 {
46 votes_.reserve(numPeers);
47 }
48
50 [[nodiscard]] TxID_t const&
51 id() const
52 {
53 return tx_.id();
54 }
55
57 [[nodiscard]] bool
58 getOurVote() const
59 {
60 return ourVote_;
61 }
62
65 [[nodiscard]] bool
67 ConsensusParms const& p,
68 bool proposing,
69 int peersUnchanged,
71 std::unique_ptr<std::stringstream> const& clog) const
72 {
73 // at() can throw, but the map is built by hand to ensure all valid
74 // values are available.
75 auto const& currentCutoff = p.avalancheCutoffs.at(avalancheState_);
76 auto const& nextCutoff = p.avalancheCutoffs.at(currentCutoff.next);
77
78 // We're have not reached the final avalanche state, or been there long
79 // enough, so there's room for change. Check the times in case the state
80 // machine is altered to allow states to loop.
81 if (nextCutoff.consensusTime > currentCutoff.consensusTime ||
83 return false;
84
85 // We've haven't had this vote for minimum rounds yet. Things could
86 // change.
87 if (proposing && currentVoteCounter_ < p.avMinRounds)
88 return false;
89
90 // If we or any peers have changed a vote in several rounds, then
91 // things could still change. But if _either_ has not changed in that
92 // long, we're unlikely to change our vote any time soon. (This prevents
93 // a malicious peer from flip-flopping a vote to prevent consensus.)
94 if (peersUnchanged < p.avStalledRounds &&
95 (proposing && currentVoteCounter_ < p.avStalledRounds))
96 return false;
97
98 // Does this transaction have more than 80% agreement
99
100 // Compute the percentage of nodes voting 'yes' (possibly including us)
101 int const support = (yays_ + (proposing && ourVote_ ? 1 : 0)) * 100;
102 int const total = nays_ + yays_ + (proposing ? 1 : 0);
103 if (total == 0)
104 {
105 // There are no votes, so we know nothing
106 return false;
107 }
108 int const weight = support / total;
109 // Returns true if the tx has more than minCONSENSUS_PCT (80) percent
110 // agreement. Either voting for _or_ voting against the tx.
111 bool const stalled = weight > p.minConsensusPct || weight < (100 - p.minConsensusPct);
112
113 if (stalled)
114 {
115 // stalling is an error condition for even a single
116 // transaction.
118 s << "Transaction " << id() << " is stalled. We have been voting "
119 << (getOurVote() ? "YES" : "NO") << " for " << currentVoteCounter_
120 << " rounds. Peers have not changed their votes in " << peersUnchanged
121 << " rounds. The transaction has " << weight << "% support. ";
122 JLOG(j_.error()) << s.str();
123 CLOG(clog) << s.str();
124 }
125
126 return stalled;
127 }
128
130 [[nodiscard]] Tx const&
131 tx() const
132 {
133 return tx_;
134 }
135
137 void
139 {
140 ourVote_ = o;
141 }
142
151 [[nodiscard]] bool
152 setVote(NodeId const& peer, bool votesYes);
153
158 void
159 unVote(NodeId const& peer);
160
172 bool
173 updateVote(int percentTime, bool proposing, ConsensusParms const& p);
174
176 [[nodiscard]] json::Value
177 getJson() const;
178
179private:
180 int yays_{0}; //< Number of yes votes
181 int nays_{0}; //< Number of no votes
182 bool ourVote_; //< Our vote (true is yes)
183 Tx tx_; //< Transaction under dispute
184 Map_t votes_; //< Map from NodeID to vote
192};
193
194// Track a peer's yes/no vote on a particular disputed tx_
195template <class Tx, class NodeId>
196bool
197DisputedTx<Tx, NodeId>::setVote(NodeId const& peer, bool votesYes)
198{
199 auto const [it, inserted] = votes_.insert(std::make_pair(peer, votesYes));
200
201 // new vote
202 if (inserted)
203 {
204 if (votesYes)
205 {
206 JLOG(j_.debug()) << "Peer " << peer << " votes YES on " << tx_.id();
207 ++yays_;
208 }
209 else
210 {
211 JLOG(j_.debug()) << "Peer " << peer << " votes NO on " << tx_.id();
212 ++nays_;
213 }
214 return true;
215 }
216 // changes vote to yes
217 if (votesYes && !it->second)
218 {
219 JLOG(j_.debug()) << "Peer " << peer << " now votes YES on " << tx_.id();
220 --nays_;
221 ++yays_;
222 it->second = true;
223 return true;
224 }
225 // changes vote to no
226 if (!votesYes && it->second)
227 {
228 JLOG(j_.debug()) << "Peer " << peer << " now votes NO on " << tx_.id();
229 ++nays_;
230 --yays_;
231 it->second = false;
232 return true;
233 }
234 return false;
235}
236
237// Remove a peer's vote on this disputed transaction
238template <class Tx, class NodeId>
239void
241{
242 auto it = votes_.find(peer);
243
244 if (it != votes_.end())
245 {
246 if (it->second)
247 {
248 --yays_;
249 }
250 else
251 {
252 --nays_;
253 }
254
255 votes_.erase(it);
256 }
257}
258
259template <class Tx, class NodeId>
260bool
261DisputedTx<Tx, NodeId>::updateVote(int percentTime, bool proposing, ConsensusParms const& p)
262{
263 if (ourVote_ && (nays_ == 0))
264 return false;
265
266 if (!ourVote_ && (yays_ == 0))
267 return false;
268
269 bool newPosition = false;
270 int weight = 0;
271
272 // When proposing, to prevent avalanche stalls, we increase the needed
273 // weight slightly over time. We also need to ensure that the consensus has
274 // made a minimum number of attempts at each "state" before moving
275 // to the next.
276 // Proposing or not, we need to keep track of which state we've reached so
277 // we can determine if the vote has stalled.
278 auto const [requiredPct, newState] =
280 if (newState)
281 {
282 avalancheState_ = *newState;
284 }
285
286 if (proposing) // give ourselves full weight
287 {
288 // This is basically the percentage of nodes voting 'yes' (including us)
289 weight = ((yays_ * 100) + (ourVote_ ? 100 : 0)) / (nays_ + yays_ + 1);
290
291 newPosition = weight > requiredPct;
292 }
293 else
294 {
295 // don't let us outweigh a proposing node, just recognize consensus
296 weight = -1;
297 newPosition = yays_ > nays_;
298 }
299
300 if (newPosition == ourVote_)
301 {
303 JLOG(j_.info()) << "No change (" << (ourVote_ ? "YES" : "NO") << ") on " << tx_.id()
304 << " : weight " << weight << ", percent " << percentTime
305 << ", round(s) with this vote: " << currentVoteCounter_;
306 JLOG(j_.debug()) << json::Compact{getJson()};
307 return false;
308 }
309
311 ourVote_ = newPosition;
312 JLOG(j_.debug()) << "We now vote " << (ourVote_ ? "YES" : "NO") << " on " << tx_.id();
313 JLOG(j_.debug()) << json::Compact{getJson()};
314 return true;
315}
316
317template <class Tx, class NodeId>
320{
321 using std::to_string;
322
324
325 ret["yays"] = yays_;
326 ret["nays"] = nays_;
327 ret["our_vote"] = ourVote_;
328
329 if (!votes_.empty())
330 {
332 for (auto const& [nodeId, vote] : votes_)
333 votes[to_string(nodeId)] = vote;
334 ret["votes"] = std::move(votes);
335 }
336
337 return ret;
338}
339
340} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:38
Decorator for streaming out compact json.
Represents a JSON value.
Definition json_value.h:130
ConsensusParms::AvalancheState avalancheState_
Definition DisputedTx.h:188
void setOurVote(bool o)
Change our vote.
Definition DisputedTx.h:138
TxID_t const & id() const
The unique id/hash of the disputed transaction.
Definition DisputedTx.h:51
boost::container::flat_map< NodeID_t, bool > Map_t
Definition DisputedTx.h:33
json::Value getJson() const
JSON representation of dispute, used for debugging.
Definition DisputedTx.h:319
void unVote(NodeId const &peer)
Remove a peer's vote.
Definition DisputedTx.h:240
bool updateVote(int percentTime, bool proposing, ConsensusParms const &p)
Update our vote given progression of consensus.
Definition DisputedTx.h:261
bool setVote(NodeId const &peer, bool votesYes)
Change a peer's vote.
Definition DisputedTx.h:197
DisputedTx(Tx tx, bool ourVote, std::size_t numPeers, beast::Journal j)
Constructor.
Definition DisputedTx.h:43
bool stalled(ConsensusParms const &p, bool proposing, int peersUnchanged, beast::Journal j, std::unique_ptr< std::stringstream > const &clog) const
Definition DisputedTx.h:66
bool getOurVote() const
Our vote on whether the transaction should be included.
Definition DisputedTx.h:58
T make_pair(T... args)
@ Object
object value (collection of name/value pairs).
Definition json_value.h:26
STL namespace.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
json::Value getJson(LedgerFill const &fill)
Return a new json::Value representing the ledger with given options.
std::pair< std::size_t, std::optional< ConsensusParms::AvalancheState > > getNeededWeight(ConsensusParms const &p, ConsensusParms::AvalancheState currentState, int percentTime, std::size_t currentRounds, std::size_t minimumRounds)
T str(T... args)
Consensus algorithm parameters.
std::size_t const avMinRounds
Number of rounds before certain actions can happen.
std::size_t const avStalledRounds
std::map< AvalancheState, AvalancheCutoff > const avalancheCutoffs
std::size_t const minConsensusPct
The percentage threshold above which we can declare consensus.
T to_string(T... args)