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 : yays_(0), nays_(0), 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 || avalancheCounter_ < p.avMIN_ROUNDS)
80 return false;
81
82 // We've haven't had this vote for minimum rounds yet. Things could
83 // change.
85 return false;
86
87 // If we or any peers have changed a vote in several rounds, then
88 // things could still change. But if _either_ has not changed in that
89 // long, we're unlikely to change our vote any time soon. (This prevents
90 // a malicious peer from flip-flopping a vote to prevent consensus.)
91 if (peersUnchanged < p.avSTALLED_ROUNDS && (proposing && currentVoteCounter_ < p.avSTALLED_ROUNDS))
92 return false;
93
94 // Does this transaction have more than 80% agreement
95
96 // Compute the percentage of nodes voting 'yes' (possibly including us)
97 int const support = (yays_ + (proposing && ourVote_ ? 1 : 0)) * 100;
98 int total = nays_ + yays_ + (proposing ? 1 : 0);
99 if (!total)
100 // There are no votes, so we know nothing
101 return false;
102 int const weight = support / total;
103 // Returns true if the tx has more than minCONSENSUS_PCT (80) percent
104 // agreement. Either voting for _or_ voting against the tx.
105 bool const stalled = weight > p.minCONSENSUS_PCT || weight < (100 - p.minCONSENSUS_PCT);
106
107 if (stalled)
108 {
109 // stalling is an error condition for even a single
110 // transaction.
112 s << "Transaction " << ID() << " is stalled. We have been voting " << (getOurVote() ? "YES" : "NO")
113 << " for " << currentVoteCounter_ << " rounds. Peers have not changed their votes in " << peersUnchanged
114 << " rounds. The transaction has " << weight << "% support. ";
115 JLOG(j_.error()) << s.str();
116 CLOG(clog) << s.str();
117 }
118
119 return stalled;
120 }
121
123 Tx_t const&
124 tx() const
125 {
126 return tx_;
127 }
128
130 void
132 {
133 ourVote_ = o;
134 }
135
144 [[nodiscard]] bool
145 setVote(NodeID_t const& peer, bool votesYes);
146
151 void
152 unVote(NodeID_t const& peer);
153
165 bool
166 updateVote(int percentTime, bool proposing, ConsensusParms const& p);
167
170 getJson() const;
171
172private:
173 int yays_; //< Number of yes votes
174 int nays_; //< Number of no votes
175 bool ourVote_; //< Our vote (true is yes)
176 Tx_t tx_; //< Transaction under dispute
177 Map_t votes_; //< Map from NodeID to vote
185};
186
187// Track a peer's yes/no vote on a particular disputed tx_
188template <class Tx_t, class NodeID_t>
189bool
190DisputedTx<Tx_t, NodeID_t>::setVote(NodeID_t const& peer, bool votesYes)
191{
192 auto const [it, inserted] = votes_.insert(std::make_pair(peer, votesYes));
193
194 // new vote
195 if (inserted)
196 {
197 if (votesYes)
198 {
199 JLOG(j_.debug()) << "Peer " << peer << " votes YES on " << tx_.id();
200 ++yays_;
201 }
202 else
203 {
204 JLOG(j_.debug()) << "Peer " << peer << " votes NO on " << tx_.id();
205 ++nays_;
206 }
207 return true;
208 }
209 // changes vote to yes
210 else if (votesYes && !it->second)
211 {
212 JLOG(j_.debug()) << "Peer " << peer << " now votes YES on " << tx_.id();
213 --nays_;
214 ++yays_;
215 it->second = true;
216 return true;
217 }
218 // changes vote to no
219 else if (!votesYes && it->second)
220 {
221 JLOG(j_.debug()) << "Peer " << peer << " now votes NO on " << tx_.id();
222 ++nays_;
223 --yays_;
224 it->second = false;
225 return true;
226 }
227 return false;
228}
229
230// Remove a peer's vote on this disputed transaction
231template <class Tx_t, class NodeID_t>
232void
234{
235 auto it = votes_.find(peer);
236
237 if (it != votes_.end())
238 {
239 if (it->second)
240 --yays_;
241 else
242 --nays_;
243
244 votes_.erase(it);
245 }
246}
247
248template <class Tx_t, class NodeID_t>
249bool
251{
252 if (ourVote_ && (nays_ == 0))
253 return false;
254
255 if (!ourVote_ && (yays_ == 0))
256 return false;
257
258 bool newPosition;
259 int weight;
260
261 // When proposing, to prevent avalanche stalls, we increase the needed
262 // weight slightly over time. We also need to ensure that the consensus has
263 // made a minimum number of attempts at each "state" before moving
264 // to the next.
265 // Proposing or not, we need to keep track of which state we've reached so
266 // we can determine if the vote has stalled.
267 auto const [requiredPct, newState] =
268 getNeededWeight(p, avalancheState_, percentTime, ++avalancheCounter_, p.avMIN_ROUNDS);
269 if (newState)
270 {
271 avalancheState_ = *newState;
272 avalancheCounter_ = 0;
273 }
274
275 if (proposing) // give ourselves full weight
276 {
277 // This is basically the percentage of nodes voting 'yes' (including us)
278 weight = (yays_ * 100 + (ourVote_ ? 100 : 0)) / (nays_ + yays_ + 1);
279
280 newPosition = weight > requiredPct;
281 }
282 else
283 {
284 // don't let us outweigh a proposing node, just recognize consensus
285 weight = -1;
286 newPosition = yays_ > nays_;
287 }
288
289 if (newPosition == ourVote_)
290 {
291 ++currentVoteCounter_;
292 JLOG(j_.info()) << "No change (" << (ourVote_ ? "YES" : "NO") << ") on " << tx_.id() << " : weight " << weight
293 << ", percent " << percentTime << ", round(s) with this vote: " << currentVoteCounter_;
294 JLOG(j_.debug()) << Json::Compact{getJson()};
295 return false;
296 }
297
298 currentVoteCounter_ = 0;
299 ourVote_ = newPosition;
300 JLOG(j_.debug()) << "We now vote " << (ourVote_ ? "YES" : "NO") << " on " << tx_.id();
301 JLOG(j_.debug()) << Json::Compact{getJson()};
302 return true;
303}
304
305template <class Tx_t, class NodeID_t>
308{
309 using std::to_string;
310
312
313 ret["yays"] = yays_;
314 ret["nays"] = nays_;
315 ret["our_vote"] = ourVote_;
316
317 if (!votes_.empty())
318 {
320 for (auto const& [nodeId, vote] : votes_)
321 votes[to_string(nodeId)] = vote;
322 ret["votes"] = std::move(votes);
323 }
324
325 return ret;
326}
327
328} // 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:318
Stream debug() const
Definition Journal.h:300
Stream info() const
Definition Journal.h:306
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:179
beast::Journal const j_
Definition DisputedTx.h:184
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:250
void unVote(NodeID_t const &peer)
Remove a peer's vote.
Definition DisputedTx.h:233
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:124
Json::Value getJson() const
JSON representation of dispute, used for debugging.
Definition DisputedTx.h:307
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:183
bool setVote(NodeID_t const &peer, bool votesYes)
Change a peer's vote.
Definition DisputedTx.h:190
ConsensusParms::AvalancheState avalancheState_
Which minimum acceptance percentage phase we are currently in.
Definition DisputedTx.h:181
void setOurVote(bool o)
Change our vote.
Definition DisputedTx.h:131
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:597
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)