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