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