xrpld
Loading...
Searching...
No Matches
DisputedTx.h
1#pragma once
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/beast/utility/Journal.h>
5#include <xrpl/consensus/ConsensusParms.h>
6#include <xrpl/json/json_value.h>
7#include <xrpl/json/json_writer.h>
8
9#include <boost/container/flat_map.hpp>
10
11#include <cstddef>
12#include <memory>
13#include <sstream>
14#include <string>
15#include <utility>
16
17namespace xrpl {
18
33
34template <class Tx, class NodeId>
36{
37 using TxID_t = Tx::ID;
38 using Map_t = boost::container::flat_map<NodeId, bool>;
39
40public:
49 DisputedTx(Tx tx, bool ourVote, std::size_t numPeers, beast::Journal j)
50 : ourVote_(ourVote), tx_(std::move(tx)), j_(j)
51 {
52 votes_.reserve(numPeers);
53 }
54
58 [[nodiscard]] TxID_t const&
59 id() const
60 {
61 return tx_.id();
62 }
63
67 [[nodiscard]] bool
68 getOurVote() const
69 {
70 return ourVote_;
71 }
72
77 [[nodiscard]] bool
79 ConsensusParms const& p,
80 bool proposing,
81 int peersUnchanged,
83 std::unique_ptr<std::stringstream> const& clog) const
84 {
85 // at() can throw, but the map is built by hand to ensure all valid
86 // values are available.
87 auto const& currentCutoff = p.avalancheCutoffs.at(avalancheState_);
88 auto const& nextCutoff = p.avalancheCutoffs.at(currentCutoff.next);
89
90 // We're have not reached the final avalanche state, or been there long
91 // enough, so there's room for change. Check the times in case the state
92 // machine is altered to allow states to loop.
93 if (nextCutoff.consensusTime > currentCutoff.consensusTime ||
95 return false;
96
97 // We've haven't had this vote for minimum rounds yet. Things could
98 // change.
99 if (proposing && currentVoteCounter_ < p.avMinRounds)
100 return false;
101
102 // If we or any peers have changed a vote in several rounds, then
103 // things could still change. But if _either_ has not changed in that
104 // long, we're unlikely to change our vote any time soon. (This prevents
105 // a malicious peer from flip-flopping a vote to prevent consensus.)
106 if (peersUnchanged < p.avStalledRounds &&
107 (proposing && currentVoteCounter_ < p.avStalledRounds))
108 return false;
109
110 // Does this transaction have more than 80% agreement
111
112 // Compute the percentage of nodes voting 'yes' (possibly including us)
113 int const support = (yays_ + (proposing && ourVote_ ? 1 : 0)) * 100;
114 int const total = nays_ + yays_ + (proposing ? 1 : 0);
115 if (total == 0)
116 {
117 // There are no votes, so we know nothing
118 return false;
119 }
120 int const weight = support / total;
121 // Returns true if the tx has more than minCONSENSUS_PCT (80) percent
122 // agreement. Either voting for _or_ voting against the tx.
123 bool const stalled = weight > p.minConsensusPct || weight < (100 - p.minConsensusPct);
124
125 if (stalled)
126 {
127 // stalling is an error condition for even a single
128 // transaction.
130 s << "Transaction " << id() << " is stalled. We have been voting "
131 << (getOurVote() ? "YES" : "NO") << " for " << currentVoteCounter_
132 << " rounds. Peers have not changed their votes in " << peersUnchanged
133 << " rounds. The transaction has " << weight << "% support. ";
134 JLOG(j_.error()) << s.str();
135 CLOG(clog) << s.str();
136 }
137
138 return stalled;
139 }
140
144 [[nodiscard]] Tx const&
145 tx() const
146 {
147 return tx_;
148 }
149
153 void
155 {
156 ourVote_ = o;
157 }
158
168 [[nodiscard]] bool
169 setVote(NodeId const& peer, bool votesYes);
170
176 void
177 unVote(NodeId const& peer);
178
191 bool
192 updateVote(int percentTime, bool proposing, ConsensusParms const& p);
193
197 [[nodiscard]] json::Value
198 getJson() const;
199
200private:
201 int yays_{0}; //< Number of yes votes
202 int nays_{0}; //< Number of no votes
203 bool ourVote_; //< Our vote (true is yes)
204 Tx tx_; //< Transaction under dispute
205 Map_t votes_; //< Map from NodeID to vote
219};
220
221// Track a peer's yes/no vote on a particular disputed tx_
222template <class Tx, class NodeId>
223bool
224DisputedTx<Tx, NodeId>::setVote(NodeId const& peer, bool votesYes)
225{
226 auto const [it, inserted] = votes_.insert(std::make_pair(peer, votesYes));
227
228 // new vote
229 if (inserted)
230 {
231 if (votesYes)
232 {
233 JLOG(j_.debug()) << "Peer " << peer << " votes YES on " << tx_.id();
234 ++yays_;
235 }
236 else
237 {
238 JLOG(j_.debug()) << "Peer " << peer << " votes NO on " << tx_.id();
239 ++nays_;
240 }
241 return true;
242 }
243 // changes vote to yes
244 if (votesYes && !it->second)
245 {
246 JLOG(j_.debug()) << "Peer " << peer << " now votes YES on " << tx_.id();
247 --nays_;
248 ++yays_;
249 it->second = true;
250 return true;
251 }
252 // changes vote to no
253 if (!votesYes && it->second)
254 {
255 JLOG(j_.debug()) << "Peer " << peer << " now votes NO on " << tx_.id();
256 ++nays_;
257 --yays_;
258 it->second = false;
259 return true;
260 }
261 return false;
262}
263
264// Remove a peer's vote on this disputed transaction
265template <class Tx, class NodeId>
266void
268{
269 auto it = votes_.find(peer);
270
271 if (it != votes_.end())
272 {
273 if (it->second)
274 {
275 --yays_;
276 }
277 else
278 {
279 --nays_;
280 }
281
282 votes_.erase(it);
283 }
284}
285
286template <class Tx, class NodeId>
287bool
288DisputedTx<Tx, NodeId>::updateVote(int percentTime, bool proposing, ConsensusParms const& p)
289{
290 if (ourVote_ && (nays_ == 0))
291 return false;
292
293 if (!ourVote_ && (yays_ == 0))
294 return false;
295
296 bool newPosition = false;
297 int weight = 0;
298
299 // When proposing, to prevent avalanche stalls, we increase the needed
300 // weight slightly over time. We also need to ensure that the consensus has
301 // made a minimum number of attempts at each "state" before moving
302 // to the next.
303 // Proposing or not, we need to keep track of which state we've reached so
304 // we can determine if the vote has stalled.
305 auto const [requiredPct, newState] =
307 if (newState)
308 {
309 avalancheState_ = *newState;
311 }
312
313 if (proposing) // give ourselves full weight
314 {
315 // This is basically the percentage of nodes voting 'yes' (including us)
316 weight = ((yays_ * 100) + (ourVote_ ? 100 : 0)) / (nays_ + yays_ + 1);
317
318 newPosition = weight > requiredPct;
319 }
320 else
321 {
322 // don't let us outweigh a proposing node, just recognize consensus
323 weight = -1;
324 newPosition = yays_ > nays_;
325 }
326
327 if (newPosition == ourVote_)
328 {
330 JLOG(j_.info()) << "No change (" << (ourVote_ ? "YES" : "NO") << ") on " << tx_.id()
331 << " : weight " << weight << ", percent " << percentTime
332 << ", round(s) with this vote: " << currentVoteCounter_;
333 JLOG(j_.debug()) << json::Compact{getJson()};
334 return false;
335 }
336
338 ourVote_ = newPosition;
339 JLOG(j_.debug()) << "We now vote " << (ourVote_ ? "YES" : "NO") << " on " << tx_.id();
340 JLOG(j_.debug()) << json::Compact{getJson()};
341 return true;
342}
343
344template <class Tx, class NodeId>
347{
348 using std::to_string;
349
351
352 ret["yays"] = yays_;
353 ret["nays"] = nays_;
354 ret["our_vote"] = ourVote_;
355
356 if (!votes_.empty())
357 {
359 for (auto const& [nodeId, vote] : votes_)
360 votes[to_string(nodeId)] = vote;
361 ret["votes"] = std::move(votes);
362 }
363
364 return ret;
365}
366
367} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
Decorator for streaming out compact json.
Represents a JSON value.
Definition json_value.h:117
ConsensusParms::AvalancheState avalancheState_
Definition DisputedTx.h:213
void setOurVote(bool o)
Change our vote.
Definition DisputedTx.h:154
TxID_t const & id() const
The unique id/hash of the disputed transaction.
Definition DisputedTx.h:59
boost::container::flat_map< NodeID_t, bool > Map_t
Definition DisputedTx.h:38
json::Value getJson() const
JSON representation of dispute, used for debugging.
Definition DisputedTx.h:346
void unVote(NodeId const &peer)
Remove a peer's vote.
Definition DisputedTx.h:267
bool updateVote(int percentTime, bool proposing, ConsensusParms const &p)
Update our vote given progression of consensus.
Definition DisputedTx.h:288
bool setVote(NodeId const &peer, bool votesYes)
Change a peer's vote.
Definition DisputedTx.h:224
DisputedTx(Tx tx, bool ourVote, std::size_t numPeers, beast::Journal j)
Constructor.
Definition DisputedTx.h:49
bool stalled(ConsensusParms const &p, bool proposing, int peersUnchanged, beast::Journal j, std::unique_ptr< std::stringstream > const &clog) const
Are we and our peers "stalled" where we probably won't change our vote?
Definition DisputedTx.h:78
bool getOurVote() const
Our vote on whether the transaction should be included.
Definition DisputedTx.h:68
T make_pair(T... args)
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
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:651
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
Number of rounds before a stuck vote is considered unlikely to change because voting stalled.
std::map< AvalancheState, AvalancheCutoff > const avalancheCutoffs
Map the consensus requirement avalanche state to the amount of time that must pass before moving to t...
std::size_t const minConsensusPct
The percentage threshold above which we can declare consensus.
T to_string(T... args)