From 706ca874b01d12a995dd8f914a713634878f1583 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Sun, 1 Mar 2020 21:03:19 -0500 Subject: [PATCH] Implement negative UNL functionality: This change can help improve the liveness of the network during periods of network instability, by allowing the network to track which validators are presently not online and to disregard them for the purposes of quorum calculations. --- Builds/CMake/RippledCore.cmake | 2 + docs/0001-negative-unl/README.md | 597 +++++ .../negativeUNLSqDiagram.puml | 79 + .../negativeUNL_highLevel_sequence.png | Bin 0 -> 140927 bytes src/ripple/app/consensus/RCLConsensus.cpp | 68 +- src/ripple/app/consensus/RCLConsensus.h | 14 +- src/ripple/app/ledger/Ledger.cpp | 124 + src/ripple/app/ledger/Ledger.h | 45 + src/ripple/app/ledger/impl/BuildLedger.cpp | 5 + src/ripple/app/ledger/impl/LedgerMaster.cpp | 30 +- src/ripple/app/misc/FeeVoteImpl.cpp | 3 +- src/ripple/app/misc/NegativeUNLVote.cpp | 350 +++ src/ripple/app/misc/NegativeUNLVote.h | 217 ++ src/ripple/app/misc/NetworkOPs.cpp | 6 +- src/ripple/app/misc/ValidatorList.h | 50 +- src/ripple/app/misc/impl/ValidatorList.cpp | 101 +- src/ripple/app/tx/impl/Change.cpp | 165 +- src/ripple/app/tx/impl/Change.h | 3 + src/ripple/app/tx/impl/InvariantCheck.cpp | 1 + src/ripple/app/tx/impl/applySteps.cpp | 7 +- src/ripple/consensus/Validations.h | 37 +- src/ripple/proto/org/xrpl/rpc/v1/common.proto | 25 +- .../org/xrpl/rpc/v1/ledger_objects.proto | 16 +- src/ripple/protocol/Feature.h | 5 +- src/ripple/protocol/Indexes.h | 4 + src/ripple/protocol/LedgerFormats.h | 2 + src/ripple/protocol/SField.h | 9 +- src/ripple/protocol/TxFormats.h | 1 + src/ripple/protocol/impl/Feature.cpp | 7 +- src/ripple/protocol/impl/Indexes.cpp | 9 + src/ripple/protocol/impl/LedgerFormats.cpp | 9 + src/ripple/protocol/impl/SField.cpp | 14 +- src/ripple/protocol/impl/STTx.cpp | 2 +- src/ripple/protocol/impl/TxFormats.cpp | 9 + src/ripple/protocol/jss.h | 5 +- src/ripple/rpc/impl/GRPCHelpers.cpp | 62 + src/ripple/rpc/impl/GRPCHelpers.h | 3 + src/test/app/ValidatorList_test.cpp | 180 ++ src/test/consensus/NegativeUNL_test.cpp | 2108 +++++++++++++++++ src/test/consensus/Validations_test.cpp | 10 +- src/test/rpc/ValidatorRPC_test.cpp | 33 + 41 files changed, 4344 insertions(+), 73 deletions(-) create mode 100644 docs/0001-negative-unl/README.md create mode 100644 docs/0001-negative-unl/negativeUNLSqDiagram.puml create mode 100644 docs/0001-negative-unl/negativeUNL_highLevel_sequence.png create mode 100644 src/ripple/app/misc/NegativeUNLVote.cpp create mode 100644 src/ripple/app/misc/NegativeUNLVote.h create mode 100644 src/test/consensus/NegativeUNL_test.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 8c45ae6208..ba8a4db8d2 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -377,6 +377,7 @@ target_sources (rippled PRIVATE src/ripple/app/misc/CanonicalTXSet.cpp src/ripple/app/misc/FeeVoteImpl.cpp src/ripple/app/misc/HashRouter.cpp + src/ripple/app/misc/NegativeUNLVote.cpp src/ripple/app/misc/NetworkOPs.cpp src/ripple/app/misc/SHAMapStoreImp.cpp src/ripple/app/misc/impl/AccountTxPaging.cpp @@ -746,6 +747,7 @@ target_sources (rippled PRIVATE src/test/consensus/DistributedValidatorsSim_test.cpp src/test/consensus/LedgerTiming_test.cpp src/test/consensus/LedgerTrie_test.cpp + src/test/consensus/NegativeUNL_test.cpp src/test/consensus/ScaleFreeSim_test.cpp src/test/consensus/Validations_test.cpp #[===============================[ diff --git a/docs/0001-negative-unl/README.md b/docs/0001-negative-unl/README.md new file mode 100644 index 0000000000..606b30aab1 --- /dev/null +++ b/docs/0001-negative-unl/README.md @@ -0,0 +1,597 @@ +# Negative UNL Engineering Spec + +## The Problem Statement + +The moment-to-moment health of the XRP Ledger network depends on the health and +connectivity of a small number of computers (nodes). The most important nodes +are validators, specifically ones listed on the unique node list +([UNL](#Question-What-are-UNLs)). Ripple publishes a recommended UNL that most +network nodes use to determine which peers in the network are trusted. Although +most validators use the same list, they are not required to. The XRP Ledger +network progresses to the next ledger when enough validators reach agreement +(above the minimum quorum of 80%) about what transactions to include in the next +ledger. + +As an example, if there are 10 validators on the UNL, at least 8 validators have +to agree with the latest ledger for it to become validated. But what if enough +of those validators are offline to drop the network below the 80% quorum? The +XRP Ledger network favors safety/correctness over advancing the ledger. Which +means if enough validators are offline, the network will not be able to validate +ledgers. + +Unfortunately validators can go offline at any time for many different reasons. +Power outages, network connectivity issues, and hardware failures are just a few +scenarios where a validator would appear "offline". Given that most of these +events are temporary, it would make sense to temporarily remove that validator +from the UNL. But the UNL is updated infrequently and not every node uses the +same UNL. So instead of removing the unreliable validator from the Ripple +recommended UNL, we can create a second negative UNL which is stored directly on +the ledger (so the entire network has the same view). This will help the network +see which validators are **currently** unreliable, and adjust their quorum +calculation accordingly. + +*Improving the liveness of the network is the main motivation for the negative UNL.* + +### Targeted Faults + +In order to determine which validators are unreliable, we need clearly define +what kind of faults to measure and analyze. We want to deal with the faults we +frequently observe in the production network. Hence we will only monitor for +validators that do not reliably respond to network messages or send out +validations disagreeing with the locally generated validations. We will not +target other byzantine faults. + +To track whether or not a validator is responding to the network, we could +monitor them with a “heartbeat” protocol. Instead of creating a new heartbeat +protocol, we can leverage some existing protocol messages to mimic the +heartbeat. We picked validation messages because validators should send one and +only one validation message per ledger. In addition, we only count the +validation messages that agree with the local node's validations. + +With the negative UNL, the network could keep making forward progress safely +even if the number of remaining validators gets to 60%. Say we have a network +with 10 validators on the UNL and everything is operating correctly. The quorum +required for this network would be 8 (80% of 10). When validators fail, the +quorum required would be as low as 6 (60% of 10), which is the absolute +***minimum quorum***. We need the absolute minimum quorum to be strictly greater +than 50% of the original UNL so that there cannot be two partitions of +well-behaved nodes headed in different directions. We arbitrarily choose 60% as +the minimum quorum to give a margin of safety. + +Consider these events in the absence of negative UNL: +1. 1:00pm - validator1 fails, votes vs. quorum: 9 >= 8, we have quorum +1. 3:00pm - validator2 fails, votes vs. quorum: 8 >= 8, we have quorum +1. 5:00pm - validator3 fails, votes vs. quorum: 7 < 8, we don’t have quorum + * **network cannot validate new ledgers with 3 failed validators** + +We're below 80% agreement, so new ledgers cannot be validated. This is how the +XRP Ledger operates today, but if the negative UNL was enabled, the events would +happen as follows. (Please note that the events below are from a simplified +version of our protocol.) + +1. 1:00pm - validator1 fails, votes vs. quorum: 9 >= 8, we have quorum +1. 1:40pm - network adds validator1 to negative UNL, quorum changes to ceil(9 * 0.8), or 8 +1. 3:00pm - validator2 fails, votes vs. quorum: 8 >= 8, we have quorum +1. 3:40pm - network adds validator2 to negative UNL, quorum changes to ceil(8 * 0.8), or 7 +1. 5:00pm - validator3 fails, votes vs. quorum: 7 >= 7, we have quorum +1. 5:40pm - network adds validator3 to negative UNL, quorum changes to ceil(7 * 0.8), or 6 +1. 7:00pm - validator4 fails, votes vs. quorum: 6 >= 6, we have quorum + * **network can still validate new ledgers with 4 failed validators** + +## External Interactions + +### Message Format Changes +This proposal will: +1. add a new pseudo-transaction type +1. add the negative UNL to the ledger data structure. + +Any tools or systems that rely on the format of this data will have to be +updated. + +### Amendment +This feature **will** need an amendment to activate. + +## Design + +This section discusses the following topics about the Negative UNL design: + +* [Negative UNL protocol overview](#Negative-UNL-Protocol-Overview) +* [Validator reliability measurement](#Validator-Reliability-Measurement) +* [Format Changes](#Format-Changes) +* [Negative UNL maintenance](#Negative-UNL-Maintenance) +* [Quorum size calculation](#Quorum-Size-Calculation) +* [Filter validation messages](#Filter-Validation-Messages) +* [High level sequence diagram of code + changes](#High-Level-Sequence-Diagram-of-Code-Changes) + +### Negative UNL Protocol Overview + +Every ledger stores a list of zero or more unreliable validators. Updates to the +list must be approved by the validators using the consensus mechanism that +validators use to agree on the set of transactions. The list is used only when +checking if a ledger is fully validated. If a validator V is in the list, nodes +with V in their UNL adjust the quorum and V’s validation message is not counted +when verifying if a ledger is fully validated. V’s flow of messages and network +interactions, however, will remain the same. + +We define the ***effective UNL** = original UNL - negative UNL*, and the +***effective quorum*** as the quorum of the *effective UNL*. And we set +*effective quorum = Ceiling(80% * effective UNL)*. + +### Validator Reliability Measurement + +A node only measures the reliability of validators on its own UNL, and only +proposes based on local observations. There are many metrics that a node can +measure about its validators, but we have chosen ledger validation messages. +This is because every validator shall send one and only one signed validation +message per ledger. This keeps the measurement simple and removes +timing/clock-sync issues. A node will measure the percentage of agreeing +validation messages (*PAV*) received from each validator on the node's UNL. Note +that the node will only count the validation messages that agree with its own +validations. + +We define the **PAV** as the **P**ercentage of **A**greed **V**alidation +messages received for the last N ledgers, where N = 256 by default. + +When the PAV drops below the ***low-water mark***, the validator is considered +unreliable, and is a candidate to be disabled by being added to the negative +UNL. A validator must have a PAV higher than the ***high-water mark*** to be +re-enabled. The validator is re-enabled by removing it from the negative UNL. In +the implementation, we plan to set the low-water mark as 50% and the high-water +mark as 80%. + +### Format Changes + +The negative UNL component in a ledger contains three fields. +* ***NegativeUNL***: The current negative UNL, a list of unreliable validators. +* ***ToDisable***: The validator to be added to the negative UNL on the next + flag ledger. +* ***ToReEnable***: The validator to be removed from the negative UNL on the + next flag ledger. + +All three fields are optional. When the *ToReEnable* field exists, the +*NegativeUNL* field cannot be empty. + +A new pseudo-transaction, ***UNLModify***, is added. It has three fields +* ***Disabling***: A flag indicating whether the modification is to disable or + to re-enable a validator. +* ***Seq***: The ledger sequence number. +* ***Validator***: The validator to be disabled or re-enabled. + +There would be at most one *disable* `UNLModify` and one *re-enable* `UNLModify` +transaction per flag ledger. The full machinery is described further on. + +### Negative UNL Maintenance + +The negative UNL can only be modified on the flag ledgers. If a validator's +reliability status changes, it takes two flag ledgers to modify the negative +UNL. Let's see an example of the algorithm: + +* Ledger seq = 100: A validator V goes offline. +* Ledger seq = 256: This is a flag ledger, and V's reliability measurement *PAV* + is lower than the low-water mark. Other validators add `UNLModify` + pseudo-transactions `{true, 256, V}` to the transaction set which goes through + the consensus. Then the pseudo-transaction is applied to the negative UNL + ledger component by setting `ToDisable = V`. +* Ledger seq = 257 ~ 511: The negative UNL ledger component is copied from the + parent ledger. +* Ledger seq=512: This is a flag ledger, and the negative UNL is updated + `NegativeUNL = NegativeUNL + ToDisable`. + +The negative UNL may have up to `MaxNegativeListed = floor(original UNL * 25%)` +validators. The 25% is because of 75% * 80% = 60%, where 75% = 100% - 25%, 80% +is the quorum of the effective UNL, and 60% is the absolute minimum quorum of +the original UNL. Adding more than 25% validators to the negative UNL does not +improve the liveness of the network, because adding more validators to the +negative UNL cannot lower the effective quorum. + +The following is the detailed algorithm: + +* **If** the ledger seq = x is a flag ledger + + 1. Compute `NegativeUNL = NegativeUNL + ToDisable - ToReEnable` if they + exist in the parent ledger + + 1. Try to find a candidate to disable if `sizeof NegativeUNL < MaxNegativeListed` + + 1. Find a validator V that has a *PAV* lower than the low-water + mark, but is not in `NegativeUNL`. + + 1. If two or more are found, their public keys are XORed with the hash + of the parent ledger and the one with the lowest XOR result is chosen. + + 1. If V is found, create a `UNLModify` pseudo-transaction + `TxDisableValidator = {true, x, V}` + + 1. Try to find a candidate to re-enable if `sizeof NegativeUNL > 0`: + + 1. Find a validator U that is in `NegativeUNL` and has a *PAV* higher + than the high-water mark. + + 1. If U is not found, try to find one in `NegativeUNL` but not in the + local *UNL*. + + 1. If two or more are found, their public keys are XORed with the hash + of the parent ledger and the one with the lowest XOR result is chosen. + + 1. If U is found, create a `UNLModify` pseudo-transaction + `TxReEnableValidator = {false, x, U}` + + 1. If any `UNLModify` pseudo-transactions are created, add them to the + transaction set. The transaction set goes through the consensus algorithm. + + 1. If have enough support, the `UNLModify` pseudo-transactions remain in the + transaction set agreed by the validators. Then the pseudo-transactions are + applied to the ledger: + + 1. If have `TxDisableValidator`, set `ToDisable=TxDisableValidator.V`. + Else clear `ToDisable`. + + 1. If have `TxReEnableValidator`, set + `ToReEnable=TxReEnableValidator.U`. Else clear `ToReEnable`. + +* **Else** (not a flag ledger) + + 1. Copy the negative UNL ledger component from the parent ledger + +The negative UNL is stored on each ledger because we don't know when a validator +may reconnect to the network. If the negative UNL was stored only on every flag +ledger, then a new validator would have to wait until it acquires the latest +flag ledger to know the negative UNL. So any new ledgers created that are not +flag ledgers copy the negative UNL from the parent ledger. + +Note that when we have a validator to disable and a validator to re-enable at +the same flag ledger, we create two separate `UNLModify` pseudo-transactions. We +want either one or the other or both to make it into the ledger on their own +merits. + +Readers may have noticed that we defined several rules of creating the +`UNLModify` pseudo-transactions but did not describe how to enforce the rules. +The rules are actually enforced by the existing consensus algorithm. Unless +enough validators propose the same pseudo-transaction it will not be included in +the transaction set of the ledger. + +### Quorum Size Calculation + +The effective quorum is 80% of the effective UNL. Note that because at most 25% +of the original UNL can be on the negative UNL, the quorum should not be lower +than the absolute minimum quorum (i.e. 60%) of the original UNL. However, +considering that different nodes may have different UNLs, to be safe we compute +`quorum = Ceiling(max(60% * original UNL, 80% * effective UNL))`. + +### Filter Validation Messages + +If a validator V is in the negative UNL, it still participates in consensus +sessions in the same way, i.e. V still follows the protocol and publishes +proposal and validation messages. The messages from V are still stored the same +way by everyone, used to calculate the new PAV for V, and could be used in +future consensus sessions if needed. However V's ledger validation message is +not counted when checking if the ledger is fully validated. + +### High Level Sequence Diagram of Code Changes + +The diagram below is the sequence of one round of consensus. Classes and +components with non-trivial changes are colored green. + +* The `ValidatorList` class is modified to compute the quorum of the effective + UNL. + +* The `Validations` class provides an interface for querying the validation + messages from trusted validators. + +* The `ConsensusAdaptor` component: + + * The `RCLConsensus::Adaptor` class is modified for creating `UNLModify` + Pseudo-Transactions. + + * The `Change` class is modified for applying `UNLModify` + Pseudo-Transactions. + + * The `Ledger` class is modified for creating and adjusting the negative UNL + ledger component. + + * The `LedgerMaster` class is modified for filtering out validation messages + from negative UNL validators when verifying if a ledger is fully + validated. + +![Sequence diagram](./negativeUNL_highLevel_sequence.png?raw=true "Negative UNL + Changes") + + +## Roads Not Taken + +### Use a Mechanism Like Fee Voting to Process UNLModify Pseudo-Transactions + +The previous version of the negative UNL specification used the same mechanism +as the [fee voting](https://xrpl.org/fee-voting.html#voting-process.) for +creating the negative UNL, and used the negative UNL as soon as the ledger was +fully validated. However the timing of fully validation can differ among nodes, +so different negative UNLs could be used, resulting in different effective UNLs +and different quorums for the same ledger. As a result, the network's safety is +impacted. + +This updated version does not impact safety though operates a bit more slowly. +The negative UNL modifications in the *UNLModify* pseudo-transaction approved by +the consensus will take effect at the next flag ledger. The extra time of the +256 ledgers should be enough for nodes to be in sync of the negative UNL +modifications. + +### Use an Expiration Approach to Re-enable Validators + +After a validator disabled by the negative UNL becomes reliable, other +validators explicitly vote for re-enabling it. An alternative approach to +re-enable a validator is the expiration approach, which was considered in the +previous version of the specification. In the expiration approach, every entry +in the negative UNL has a fixed expiration time. One flag ledger interval was +chosen as the expiration interval. Once expired, the other validators must +continue voting to keep the unreliable validator on the negative UNL. The +advantage of this approach is its simplicity. But it has a requirement. The +negative UNL protocol must be able to vote multiple unreliable validators to be +disabled at the same flag ledger. In this version of the specification, however, +only one unreliable validator can be disabled at a flag ledger. So the +expiration approach cannot be simply applied. + +### Validator Reliability Measurement and Flag Ledger Frequency + +If the ledger time is about 4.5 seconds and the low-water mark is 50%, then in +the worst case, it takes 48 minutes *((0.5 * 256 + 256 + 256) * 4.5 / 60 = 48)* +to put an offline validator on the negative UNL. We considered lowering the flag +ledger frequency so that the negative UNL can be more responsive. We also +considered decoupling the reliability measurement and flag ledger frequency to +be more flexible. In practice, however, their benefits are not clear. + + +## New Attack Vectors + +A group of malicious validators may try to frame a reliable validator and put it +on the negative UNL. But they cannot succeed. Because: + +1. A reliable validator sends a signed validation message every ledger. A +sufficient peer-to-peer network will propagate the validation messages to other +validators. The validators will decide if another validator is reliable or not +only by its local observation of the validation messages received. So an honest +validator’s vote on another validator’s reliability is accurate. + +1. Given the votes are accurate, and one vote per validator, an honest validator +will not create a UNLModify transaction of a reliable validator. + +1. A validator can be added to a negative UNL only through a UNLModify +transaction. + +Assuming the group of malicious validators is less than the quorum, they cannot +frame a reliable validator. + +## Summary + +The bullet points below briefly summarize the current proposal: + +* The motivation of the negative UNL is to improve the liveness of the network. + +* The targeted faults are the ones frequently observed in the production + network. + +* Validators propose negative UNL candidates based on their local measurements. + +* The absolute minimum quorum is 60% of the original UNL. + +* The format of the ledger is changed, and a new *UNLModify* pseudo-transaction + is added. Any tools or systems that rely on the format of these data will have + to be updated. + +* The negative UNL can only be modified on the flag ledgers. + +* At most one validator can be added to the negative UNL at a flag ledger. + +* At most one validator can be removed from the negative UNL at a flag ledger. + +* If a validator's reliability status changes, it takes two flag ledgers to + modify the negative UNL. + +* The quorum is the larger of 80% of the effective UNL and 60% of the original + UNL. + +* If a validator is on the negative UNL, its validation messages are ignored + when the local node verifies if a ledger is fully validated. + +## FAQ + +### Question: What are UNLs? + +Quote from the [Technical FAQ](https://xrpl.org/technical-faq.html): "They are +the lists of transaction validators a given participant believes will not +conspire to defraud them." + +### Question: How does the negative UNL proposal affect network liveness? + +The network can make forward progress when more than a quorum of the trusted +validators agree with the progress. The lower the quorum size is, the easier for +the network to progress. If the quorum is too low, however, the network is not +safe because nodes may have different results. So the quorum size used in the +consensus protocol is a balance between the safety and the liveness of the +network. The negative UNL reduces the size of the effective UNL, resulting in a +lower quorum size while keeping the network safe. + +

Question: How does a validator get into the negative UNL? How is a +validator removed from the negative UNL?

+ +A validator’s reliability is measured by other validators. If a validator +becomes unreliable, at a flag ledger, other validators propose *UNLModify* +pseudo-transactions which vote the validator to add to the negative UNL during +the consensus session. If agreed, the validator is added to the negative UNL at +the next flag ledger. The mechanism of removing a validator from the negative +UNL is the same. + +### Question: Given a negative UNL, what happens if the UNL changes? + +Answer: Let’s consider the cases: + +1. A validator is added to the UNL, and it is already in the negative UNL. This +case could happen when not all the nodes have the same UNL. Note that the +negative UNL on the ledger lists unreliable nodes that are not necessarily the +validators for everyone. + + In this case, the liveness is affected negatively. Because the minimum + quorum could be larger but the usable validators are not increased. + +1. A validator is removed from the UNL, and it is in the negative UNL. + + In this case, the liveness is affected positively. Because the quorum could + be smaller but the usable validators are not reduced. + +1. A validator is added to the UNL, and it is not in the negative UNL. +1. A validator is removed from the UNL, and it is not in the negative UNL. + + Case 3 and 4 are not affected by the negative UNL protocol. + +### Question: Can we simply lower the quorum to 60% without the negative UNL? + +Answer: No, because the negative UNL approach is safer. + +First let’s compare the two approaches intuitively, (1) the *negative UNL* +approach, and (2) *lower quorum*: simply lowering the quorum from 80% to 60% +without the negative UNL. The negative UNL approach uses consensus to come up +with a list of unreliable validators, which are then removed from the effective +UNL temporarily. With this approach, the list of unreliable validators is agreed +to by a quorum of validators and will be used by every node in the network to +adjust its UNL. The quorum is always 80% of the effective UNL. The lower quorum +approach is a tradeoff between safety and liveness and against our principle of +preferring safety over liveness. Note that different validators don't have to +agree on which validation sources they are ignoring. + +Next we compare the two approaches quantitatively with examples, and apply +Theorem 8 of [Analysis of the XRP Ledger Consensus +Protocol](https://arxiv.org/abs/1802.07242) paper: + +*XRP LCP guarantees fork safety if **Oi,j > nj / 2 + +ni − qi + ti,j** for every pair of nodes +Pi, Pj,* + +where *Oi,j* is the overlapping requirement, nj and +ni are UNL sizes, qi is the quorum size of Pi, +*ti,j = min(ti, tj, Oi,j)*, and +ti and tj are the number of faults can be tolerated by +Pi and Pj. + +We denote *UNLi* as *Pi's UNL*, and *|UNLi|* as +the size of *Pi's UNL*. + +Assuming *|UNLi| = |UNLj|*, let's consider the following +three cases: + +1. With 80% quorum and 20% faults, *Oi,j > 100% / 2 + 100% - 80% + +20% = 90%*. I.e. fork safety requires > 90% UNL overlaps. This is one of the +results in the analysis paper. + +1. If the quorum is 60%, the relationship between the overlapping requirement +and the faults that can be tolerated is *Oi,j > 90% + +ti,j*. Under the same overlapping condition (i.e. 90%), to guarantee +the fork safety, the network cannot tolerate any faults. So under the same +overlapping condition, if the quorum is simply lowered, the network can tolerate +fewer faults. + +1. With the negative UNL approach, we want to argue that the inequation +*Oi,j > nj / 2 + ni − qi + +ti,j* is always true to guarantee fork safety, while the negative UNL +protocol runs, i.e. the effective quorum is lowered without weakening the +network's fault tolerance. To make the discussion easier, we rewrite the +inequation as *Oi,j > nj / 2 + (ni − +qi) + min(ti, tj)*, where Oi,j is +dropped from the definition of ti,j because *Oi,j > +min(ti, tj)* always holds under the parameters we will +use. Assuming a validator V is added to the negative UNL, now let's consider the +4 cases: + + 1. V is not on UNLi nor UNLj + + The inequation holds because none of the variables change. + + 1. V is on UNLi but not on UNLj + + The value of *(ni − qi)* is smaller. The value of + *min(ti, tj)* could be smaller too. Other + variables do not change. Overall, the left side of the inequation does + not change, but the right side is smaller. So the inequation holds. + + 1. V is not on UNLi but on UNLj + + The value of *nj / 2* is smaller. The value of + *min(ti, tj)* could be smaller too. Other + variables do not change. Overall, the left side of the inequation does + not change, but the right side is smaller. So the inequation holds. + + 1. V is on both UNLi and UNLj + + The value of *Oi,j* is reduced by 1. The values of + *nj / 2*, *(ni − qi)*, and + *min(ti, tj)* are reduced by 0.5, 0.2, and 1 + respectively. The right side is reduced by 1.7. Overall, the left side + of the inequation is reduced by 1, and the right side is reduced by 1.7. + So the inequation holds. + + The inequation holds for all the cases. So with the negative UNL approach, + the network's fork safety is preserved, while the quorum is lowered that + increases the network's liveness. + +

Question: We have observed that occasionally a validator wanders off on its +own chain. How is this case handled by the negative UNL algorithm?

+ +Answer: The case that a validator wanders off on its own chain can be measured +with the validations agreement. Because the validations by this validator must +be different from other validators' validations of the same sequence numbers. +When there are enough disagreed validations, other validators will vote this +validator onto the negative UNL. + +In general by measuring the agreement of validations, we also measured the +"sanity". If two validators have too many disagreements, one of them could be +insane. When enough validators think a validator is insane, that validator is +put on the negative UNL. + +

Question: Why would there be at most one disable UNLModify and one +re-enable UNLModify transaction per flag ledger?

+ +Answer: It is a design choice so that the effective UNL does not change too +quickly. A typical targeted scenario is several validators go offline slowly +during a long weekend. The current design can handle this kind of cases well +without changing the effective UNL too quickly. + +## Appendix + +### Confidence Test + +We will use two test networks, a single machine test network with multiple IP +addresses and the QE test network with multiple machines. The single machine +network will be used to test all the test cases and to debug. The QE network +will be used after that. We want to see the test cases still pass with real +network delay. A test case specifies: + +1. a UNL with different number of validators for different test cases, +1. a network with zero or more non-validator nodes, +1. a sequence of validator reliability change events (by killing/restarting + nodes, or by running modified rippled that does not send all validation + messages), +1. the correct outcomes. + +For all the test cases, the correct outcomes are verified by examining logs. We +will grep the log to see if the correct negative UNLs are generated, and whether +or not the network is making progress when it should be. The ripdtop tool will +be helpful for monitoring validators' states and ledger progress. Some of the +timing parameters of rippled will be changed to have faster ledger time. Most if +not all test cases do not need client transactions. + +For example, the test cases for the prototype: +1. A 10-validator UNL. +1. The network does not have other nodes. +1. The validators will be started from the genesis. Once they start to produce + ledgers, we kill five validators, one every flag ledger interval. Then we + will restart them one by one. +1. A sequence of events (or the lack of events) such as a killed validator is + added to the negative UNL. + +#### Roads Not Taken: Test with Extended CSF + +We considered testing with the current unit test framework, specifically the +[Consensus Simulation +Framework](https://github.com/ripple/rippled/blob/develop/src/test/csf/README.md) +(CSF). However, the CSF currently can only test the generic consensus algorithm +as in the paper: [Analysis of the XRP Ledger Consensus +Protocol](https://arxiv.org/abs/1802.07242). \ No newline at end of file diff --git a/docs/0001-negative-unl/negativeUNLSqDiagram.puml b/docs/0001-negative-unl/negativeUNLSqDiagram.puml new file mode 100644 index 0000000000..8cb491af6a --- /dev/null +++ b/docs/0001-negative-unl/negativeUNLSqDiagram.puml @@ -0,0 +1,79 @@ +@startuml negativeUNL_highLevel_sequence + +skinparam sequenceArrowThickness 2 +skinparam roundcorner 20 +skinparam maxmessagesize 160 + +actor "Rippled Start" as RS +participant "Timer" as T +participant "NetworkOPs" as NOP +participant "ValidatorList" as VL #lightgreen +participant "Consensus" as GC +participant "ConsensusAdaptor" as CA #lightgreen +participant "Validations" as RM #lightgreen + +RS -> NOP: begin consensus +activate NOP +NOP -[#green]> VL: update negative UNL +hnote over VL#lightgreen: store a copy of\nnegative UNL +VL -> NOP +NOP -> VL: update trusted validators +activate VL +VL -> VL: re-calculate quorum +hnote over VL#lightgreen: ignore negative listed validators\nwhen calculate quorum +VL -> NOP +deactivate VL +NOP -> GC: start round +activate GC +GC -> GC: phase = OPEN +GC -> NOP +deactivate GC +deactivate NOP + +loop at regular frequency +T -> GC: timerEntry +activate GC +end + +alt phase == OPEN + alt should close ledger + GC -> GC: phase = ESTABLISH + GC -> CA: onClose + activate CA + alt sqn%256==0 + CA -[#green]> RM: getValidations + CA -[#green]> CA: create UNLModify Tx + hnote over CA#lightgreen: use validatations of the last 256 ledgers\nto figure out UNLModify Tx candidates.\nIf any, create UNLModify Tx, and add to TxSet. + end + CA -> GC + GC -> CA: propose + deactivate CA + end +else phase == ESTABLISH + hnote over GC: receive peer postions + GC -> GC : update our position + GC -> CA : propose \n(if position changed) + GC -> GC : check if have consensus + alt consensus reached + GC -> GC: phase = ACCEPT + GC -> CA : onAccept + activate CA + CA -> CA : build LCL + hnote over CA #lightgreen: copy negative UNL from parent ledger + alt sqn%256==0 + CA -[#green]> CA: Adjust negative UNL + CA -[#green]> CA: apply UNLModify Tx + end + CA -> CA : validate and send validation message + activate NOP + CA -> NOP : end consensus and\nbegin next consensus round + deactivate NOP + deactivate CA + hnote over RM: receive validations + end +else phase == ACCEPTED + hnote over GC: timerEntry hash nothing to do at this phase +end +deactivate GC + +@enduml \ No newline at end of file diff --git a/docs/0001-negative-unl/negativeUNL_highLevel_sequence.png b/docs/0001-negative-unl/negativeUNL_highLevel_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..b962693b49ce31c1e02fc29b7c8d5fb2dc2d5c20 GIT binary patch literal 140927 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 z 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

MqoO+fX+kvZpR_mOru_CDJHI)_H3czlafff^rvN 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 F{~t7mxy=9o literal 0 HcmV?d00001 diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index f999ff4bbb..9b5ae65cd4 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -22,12 +22,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -87,8 +89,10 @@ RCLConsensus::Adaptor::Adaptor( , nodeID_{validatorKeys.nodeID} , valPublic_{validatorKeys.publicKey} , valSecret_{validatorKeys.secretKey} - , valCookie_{ - rand_int(1, std::numeric_limits::max())} + , valCookie_{rand_int( + 1, + std::numeric_limits::max())} + , nUnlVote_(nodeID_, j_) { assert(valCookie_ != 0); @@ -190,7 +194,7 @@ RCLConsensus::Adaptor::propose(RCLCxPeerPos::Proposal const& proposal) prop.set_currenttxhash( proposal.position().begin(), proposal.position().size()); prop.set_previousledger( - proposal.prevLedger().begin(), proposal.position().size()); + proposal.prevLedger().begin(), proposal.prevLedger().size()); prop.set_proposeseq(proposal.proposeSeq()); prop.set_closetime(proposal.closeTime().time_since_epoch().count()); @@ -317,18 +321,34 @@ RCLConsensus::Adaptor::onClose( } // Add pseudo-transactions to the set - if ((app_.config().standalone() || (proposing && !wrongLCL)) && - ((prevLedger->info().seq % 256) == 0)) + if (app_.config().standalone() || (proposing && !wrongLCL)) { - // previous ledger was flag ledger, add pseudo-transactions - auto const validations = app_.getValidations().getTrustedForLedger( - prevLedger->info().parentHash); - - if (validations.size() >= app_.validators().quorum()) + if (prevLedger->isFlagLedger()) { - feeVote_->doVoting(prevLedger, validations, initialSet); - app_.getAmendmentTable().doVoting( - prevLedger, validations, initialSet); + // previous ledger was flag ledger, add fee and amendment + // pseudo-transactions + auto validations = app_.validators().negativeUNLFilter( + app_.getValidations().getTrustedForLedger( + prevLedger->info().parentHash)); + if (validations.size() >= app_.validators().quorum()) + { + feeVote_->doVoting(prevLedger, validations, initialSet); + app_.getAmendmentTable().doVoting( + prevLedger, validations, initialSet); + } + } + else if ( + prevLedger->isVotingLedger() && + prevLedger->rules().enabled(featureNegativeUNL)) + { + // previous ledger was a voting ledger, + // so the current consensus session is for a flag ledger, + // add negative UNL pseudo-transactions + nUnlVote_.doVoting( + prevLedger, + app_.validators().getTrustedMasterKeys(), + app_.getValidations(), + initialSet); } } @@ -793,7 +813,7 @@ RCLConsensus::Adaptor::validate( v.setFieldU64(sfCookie, valCookie_); // Report our server version every flag ledger: - if ((ledger.seq() + 1) % 256 == 0) + if (ledger.ledger_->isVotingLedger()) v.setFieldU64( sfServerVersion, BuildInfo::getEncodedVersion()); } @@ -808,7 +828,7 @@ RCLConsensus::Adaptor::validate( // If the next ledger is a flag ledger, suggest fee changes and // new features: - if ((ledger.seq() + 1) % 256 == 0) + if (ledger.ledger_->isVotingLedger()) { // Fees: feeVote_->doValidation(ledger.ledger_->fees(), v); @@ -922,7 +942,9 @@ RCLConsensus::peerProposal( } bool -RCLConsensus::Adaptor::preStartRound(RCLCxLedger const& prevLgr) +RCLConsensus::Adaptor::preStartRound( + RCLCxLedger const& prevLgr, + hash_set const& nowTrusted) { // We have a key, we do not want out of sync validations after a restart // and are not amendment blocked. @@ -961,6 +983,11 @@ RCLConsensus::Adaptor::preStartRound(RCLCxLedger const& prevLgr) // Notify inbound ledgers that we are starting a new round inboundTransactions_.newRound(prevLgr.seq()); + // Notify NegativeUNLVote that new validators are added + if (prevLgr.ledger_->rules().enabled(featureNegativeUNL) && + !nowTrusted.empty()) + nUnlVote_.newValidators(prevLgr.seq() + 1, nowTrusted); + // propose only if we're in sync with the network (and validating) return validating_ && synced; } @@ -1009,10 +1036,15 @@ RCLConsensus::startRound( NetClock::time_point const& now, RCLCxLedger::ID const& prevLgrId, RCLCxLedger const& prevLgr, - hash_set const& nowUntrusted) + hash_set const& nowUntrusted, + hash_set const& nowTrusted) { std::lock_guard _{mutex_}; consensus_.startRound( - now, prevLgrId, prevLgr, nowUntrusted, adaptor_.preStartRound(prevLgr)); + now, + prevLgrId, + prevLgr, + nowUntrusted, + adaptor_.preStartRound(prevLgr, nowTrusted)); } } // namespace ripple diff --git a/src/ripple/app/consensus/RCLConsensus.h b/src/ripple/app/consensus/RCLConsensus.h index f06dc5e5a3..f0ab98c147 100644 --- a/src/ripple/app/consensus/RCLConsensus.h +++ b/src/ripple/app/consensus/RCLConsensus.h @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -85,6 +86,7 @@ class RCLConsensus std::atomic mode_{ConsensusMode::observing}; RCLCensorshipDetector censorshipDetector_; + NegativeUNLVote nUnlVote_; public: using Ledger_t = RCLCxLedger; @@ -131,10 +133,13 @@ class RCLConsensus /** Called before kicking off a new consensus round. @param prevLedger Ledger that will be prior ledger for next round + @param nowTrusted the new validators @return Whether we enter the round proposing */ bool - preStartRound(RCLCxLedger const& prevLedger); + preStartRound( + RCLCxLedger const& prevLedger, + hash_set const& nowTrusted); bool haveValidated() const; @@ -471,13 +476,16 @@ public: Json::Value getJson(bool full) const; - //! @see Consensus::startRound + /** Adjust the set of trusted validators and kick-off the next round of + consensus. For more details, @see Consensus::startRound + */ void startRound( NetClock::time_point const& now, RCLCxLedger::ID const& prevLgrId, RCLCxLedger const& prevLgr, - hash_set const& nowUntrusted); + hash_set const& nowUntrusted, + hash_set const& nowTrusted); //! @see Consensus::timerEntry void diff --git a/src/ripple/app/ledger/Ledger.cpp b/src/ripple/app/ledger/Ledger.cpp index 55ffb36baa..3801d99444 100644 --- a/src/ripple/app/ledger/Ledger.cpp +++ b/src/ripple/app/ledger/Ledger.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -598,6 +599,112 @@ Ledger::peek(Keylet const& k) const return sle; } +hash_set +Ledger::negativeUnl() const +{ + hash_set negUnl; + if (auto sle = read(keylet::negativeUNL()); + sle && sle->isFieldPresent(sfNegativeUNL)) + { + auto const& nUnlData = sle->getFieldArray(sfNegativeUNL); + for (auto const& n : nUnlData) + { + if (n.isFieldPresent(sfPublicKey)) + { + auto d = n.getFieldVL(sfPublicKey); + auto s = makeSlice(d); + if (!publicKeyType(s)) + { + continue; + } + negUnl.emplace(s); + } + } + } + + return negUnl; +} + +boost::optional +Ledger::negativeUnlToDisable() const +{ + if (auto sle = read(keylet::negativeUNL()); + sle && sle->isFieldPresent(sfNegativeUNLToDisable)) + { + auto d = sle->getFieldVL(sfNegativeUNLToDisable); + auto s = makeSlice(d); + if (publicKeyType(s)) + return PublicKey(s); + } + + return boost::none; +} + +boost::optional +Ledger::negativeUnlToReEnable() const +{ + if (auto sle = read(keylet::negativeUNL()); + sle && sle->isFieldPresent(sfNegativeUNLToReEnable)) + { + auto d = sle->getFieldVL(sfNegativeUNLToReEnable); + auto s = makeSlice(d); + if (publicKeyType(s)) + return PublicKey(s); + } + + return boost::none; +} + +void +Ledger::updateNegativeUNL() +{ + auto sle = peek(keylet::negativeUNL()); + if (!sle) + return; + + bool const hasToDisable = sle->isFieldPresent(sfNegativeUNLToDisable); + bool const hasToReEnable = sle->isFieldPresent(sfNegativeUNLToReEnable); + + if (!hasToDisable && !hasToReEnable) + return; + + STArray newNUnl; + if (sle->isFieldPresent(sfNegativeUNL)) + { + auto const& oldNUnl = sle->getFieldArray(sfNegativeUNL); + for (auto v : oldNUnl) + { + if (hasToReEnable && v.isFieldPresent(sfPublicKey) && + v.getFieldVL(sfPublicKey) == + sle->getFieldVL(sfNegativeUNLToReEnable)) + continue; + newNUnl.push_back(v); + } + } + + if (hasToDisable) + { + newNUnl.emplace_back(sfNegativeUNLEntry); + newNUnl.back().setFieldVL( + sfPublicKey, sle->getFieldVL(sfNegativeUNLToDisable)); + newNUnl.back().setFieldU32(sfFirstLedgerSequence, seq()); + } + + if (!newNUnl.empty()) + { + sle->setFieldArray(sfNegativeUNL, newNUnl); + if (hasToReEnable) + sle->makeFieldAbsent(sfNegativeUNLToReEnable); + if (hasToDisable) + sle->makeFieldAbsent(sfNegativeUNLToDisable); + rawReplace(sle); + } + else + { + rawErase(sle); + } +} + //------------------------------------------------------------------------------ bool Ledger::walkLedger(beast::Journal j) const @@ -735,6 +842,23 @@ Ledger::updateSkipList() rawReplace(sle); } +bool +Ledger::isFlagLedger() const +{ + return info_.seq % FLAG_LEDGER_INTERVAL == 0; +} +bool +Ledger::isVotingLedger() const +{ + return (info_.seq + 1) % FLAG_LEDGER_INTERVAL == 0; +} + +bool +isFlagLedger(LedgerIndex seq) +{ + return seq % FLAG_LEDGER_INTERVAL == 0; +} + static bool saveValidatedLedger( Application& app, diff --git a/src/ripple/app/ledger/Ledger.h b/src/ripple/app/ledger/Ledger.h index baf4e2f409..5f088651a8 100644 --- a/src/ripple/app/ledger/Ledger.h +++ b/src/ripple/app/ledger/Ledger.h @@ -329,6 +329,46 @@ public: void unshare() const; + /** + * get Negative UNL validators' master public keys + * + * @return the public keys + */ + hash_set + negativeUnl() const; + + /** + * get the to be disabled validator's master public key if any + * + * @return the public key if any + */ + boost::optional + negativeUnlToDisable() const; + + /** + * get the to be re-enabled validator's master public key if any + * + * @return the public key if any + */ + boost::optional + negativeUnlToReEnable() const; + + /** + * update the Negative UNL ledger component. + * @note must be called at and only at flag ledgers + * must be called before applying UNLModify Tx + */ + void + updateNegativeUNL(); + + /** Returns true if the ledger is a flag ledger */ + bool + isFlagLedger() const; + + /** Returns true if the ledger directly precedes a flag ledger */ + bool + isVotingLedger() const; + private: class sles_iter_impl; class txs_iter_impl; @@ -355,6 +395,11 @@ private: /** A ledger wrapped in a CachedView. */ using CachedLedger = CachedView; +std::uint32_t constexpr FLAG_LEDGER_INTERVAL = 256; +/** Returns true if the given ledgerIndex is a flag ledgerIndex */ +bool +isFlagLedger(LedgerIndex seq); + //------------------------------------------------------------------------------ // // API diff --git a/src/ripple/app/ledger/impl/BuildLedger.cpp b/src/ripple/app/ledger/impl/BuildLedger.cpp index 62aab96415..97592220eb 100644 --- a/src/ripple/app/ledger/impl/BuildLedger.cpp +++ b/src/ripple/app/ledger/impl/BuildLedger.cpp @@ -47,6 +47,11 @@ buildLedgerImpl( { auto built = std::make_shared(*parent, closeTime); + if (built->isFlagLedger() && built->rules().enabled(featureNegativeUNL)) + { + built->updateNegativeUNL(); + } + // Set up to write SHAMap changes to our database, // perform updates, extract changes diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index 7de7872afb..c72f3349bc 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -314,14 +315,14 @@ LedgerMaster::setValidLedger(std::shared_ptr const& l) if (!standalone_) { - auto const vals = - app_.getValidations().getTrustedForLedger(l->info().hash); - times.reserve(vals.size()); - for (auto const& val : vals) + auto validations = app_.validators().negativeUNLFilter( + app_.getValidations().getTrustedForLedger(l->info().hash)); + times.reserve(validations.size()); + for (auto const& val : validations) times.push_back(val->getSignTime()); - if (!vals.empty()) - consensusHash = vals.front()->getConsensusHash(); + if (!validations.empty()) + consensusHash = validations.front()->getConsensusHash(); } NetClock::time_point signTime; @@ -359,7 +360,7 @@ LedgerMaster::setValidLedger(std::shared_ptr const& l) "activated: server blocked."; app_.getOPs().setAmendmentBlocked(); } - else if (!app_.getOPs().isAmendmentWarned() || ((l->seq() % 256) == 0)) + else if (!app_.getOPs().isAmendmentWarned() || l->isFlagLedger()) { // Amendments can lose majority, so re-check periodically (every // flag ledger), and clear the flag if appropriate. If an unknown @@ -941,8 +942,9 @@ LedgerMaster::checkAccept(uint256 const& hash, std::uint32_t seq) if (seq < mValidLedgerSeq) return; - valCount = app_.getValidations().numTrustedForLedger(hash); - + auto validations = app_.validators().negativeUNLFilter( + app_.getValidations().getTrustedForLedger(hash)); + valCount = validations.size(); if (valCount >= app_.validators().quorum()) { std::lock_guard ml(m_mutex); @@ -1006,8 +1008,9 @@ LedgerMaster::checkAccept(std::shared_ptr const& ledger) return; auto const minVal = getNeededValidations(); - auto const tvc = - app_.getValidations().numTrustedForLedger(ledger->info().hash); + auto validations = app_.validators().negativeUNLFilter( + app_.getValidations().getTrustedForLedger(ledger->info().hash)); + auto const tvc = validations.size(); if (tvc < minVal) // nothing we can do { JLOG(m_journal.trace()) @@ -1162,7 +1165,8 @@ LedgerMaster::consensusBuilt( // This ledger cannot be the new fully-validated ledger, but // maybe we saved up validations for some other ledger that can be - auto const val = app_.getValidations().currentTrusted(); + auto validations = app_.validators().negativeUNLFilter( + app_.getValidations().currentTrusted()); // Track validation counts with sequence numbers class valSeq @@ -1189,7 +1193,7 @@ LedgerMaster::consensusBuilt( // Count the number of current, trusted validations hash_map count; - for (auto const& v : val) + for (auto const& v : validations) { valSeq& vs = count[v->getLedgerHash()]; vs.mergeValidation(v->getFieldU32(sfLedgerSequence)); diff --git a/src/ripple/app/misc/FeeVoteImpl.cpp b/src/ripple/app/misc/FeeVoteImpl.cpp index 873c488754..e2dc2e4071 100644 --- a/src/ripple/app/misc/FeeVoteImpl.cpp +++ b/src/ripple/app/misc/FeeVoteImpl.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -150,7 +151,7 @@ FeeVoteImpl::doVoting( std::shared_ptr const& initialPosition) { // LCL must be flag ledger - assert((lastClosedLedger->info().seq % 256) == 0); + assert(isFlagLedger(lastClosedLedger->seq())); detail::VotableValue baseFeeVote( lastClosedLedger->fees().base, target_.reference_fee); diff --git a/src/ripple/app/misc/NegativeUNLVote.cpp b/src/ripple/app/misc/NegativeUNLVote.cpp new file mode 100644 index 0000000000..f4f09b3d13 --- /dev/null +++ b/src/ripple/app/misc/NegativeUNLVote.cpp @@ -0,0 +1,350 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { + +NegativeUNLVote::NegativeUNLVote(NodeID const& myId, beast::Journal j) + : myId_(myId), j_(j) +{ +} + +void +NegativeUNLVote::doVoting( + std::shared_ptr const& prevLedger, + hash_set const& unlKeys, + RCLValidations& validations, + std::shared_ptr const& initialSet) +{ + // Voting steps: + // -- build a reliability score table of validators + // -- process the table and find all candidates to disable or to re-enable + // -- pick one to disable and one to re-enable if any + // -- if found candidates, add ttUNL_MODIFY Tx + + // Build NodeID set for internal use. + // Build NodeID to PublicKey map for lookup before creating ttUNL_MODIFY Tx. + hash_set unlNodeIDs; + hash_map nidToKeyMap; + for (auto const& k : unlKeys) + { + auto nid = calcNodeID(k); + nidToKeyMap.emplace(nid, k); + unlNodeIDs.emplace(nid); + } + + // Build a reliability score table of validators + if (std::optional> scoreTable = + buildScoreTable(prevLedger, unlNodeIDs, validations)) + { + // build next negUnl + auto negUnlKeys = prevLedger->negativeUnl(); + auto negUnlToDisable = prevLedger->negativeUnlToDisable(); + auto negUnlToReEnable = prevLedger->negativeUnlToReEnable(); + if (negUnlToDisable) + negUnlKeys.insert(*negUnlToDisable); + if (negUnlToReEnable) + negUnlKeys.erase(*negUnlToReEnable); + + hash_set negUnlNodeIDs; + for (auto const& k : negUnlKeys) + { + auto nid = calcNodeID(k); + negUnlNodeIDs.emplace(nid); + if (!nidToKeyMap.count(nid)) + { + nidToKeyMap.emplace(nid, k); + } + } + + auto const seq = prevLedger->info().seq + 1; + purgeNewValidators(seq); + + // Process the table and find all candidates to disable or to re-enable + auto const candidates = + findAllCandidates(unlNodeIDs, negUnlNodeIDs, *scoreTable); + + // Pick one to disable and one to re-enable if any, add ttUNL_MODIFY Tx + if (!candidates.toDisableCandidates.empty()) + { + auto n = + choose(prevLedger->info().hash, candidates.toDisableCandidates); + assert(nidToKeyMap.count(n)); + addTx(seq, nidToKeyMap[n], ToDisable, initialSet); + } + + if (!candidates.toReEnableCandidates.empty()) + { + auto n = choose( + prevLedger->info().hash, candidates.toReEnableCandidates); + assert(nidToKeyMap.count(n)); + addTx(seq, nidToKeyMap[n], ToReEnable, initialSet); + } + } +} + +void +NegativeUNLVote::addTx( + LedgerIndex seq, + PublicKey const& vp, + NegativeUNLModify modify, + std::shared_ptr const& initialSet) +{ + STTx negUnlTx(ttUNL_MODIFY, [&](auto& obj) { + obj.setFieldU8(sfUNLModifyDisabling, modify == ToDisable ? 1 : 0); + obj.setFieldU32(sfLedgerSequence, seq); + obj.setFieldVL(sfUNLModifyValidator, vp.slice()); + }); + + uint256 txID = negUnlTx.getTransactionID(); + Serializer s; + negUnlTx.add(s); + if (!initialSet->addGiveItem( + std::make_shared(txID, s.peekData()), true, false)) + { + JLOG(j_.warn()) << "N-UNL: ledger seq=" << seq + << ", add ttUNL_MODIFY tx failed"; + } + else + { + JLOG(j_.debug()) << "N-UNL: ledger seq=" << seq + << ", add a ttUNL_MODIFY Tx with txID: " << txID + << ", the validator to " + << (modify == ToDisable ? "disable: " : "re-enable: ") + << vp; + } +} + +NodeID +NegativeUNLVote::choose( + uint256 const& randomPadData, + std::vector const& candidates) +{ + assert(!candidates.empty()); + static_assert(NodeID::bytes <= uint256::bytes); + NodeID randomPad = NodeID::fromVoid(randomPadData.data()); + NodeID txNodeID = candidates[0]; + for (int j = 1; j < candidates.size(); ++j) + { + if ((candidates[j] ^ randomPad) < (txNodeID ^ randomPad)) + { + txNodeID = candidates[j]; + } + } + return txNodeID; +} + +std::optional> +NegativeUNLVote::buildScoreTable( + std::shared_ptr const& prevLedger, + hash_set const& unl, + RCLValidations& validations) +{ + // Find agreed validation messages received for + // the last FLAG_LEDGER_INTERVAL (i.e. 256) ledgers, + // for every validator, and fill the score table. + + // Ask the validation container to keep enough validation message history + // for next time. + auto const seq = prevLedger->info().seq + 1; + validations.setSeqToKeep(seq - 1); + + // Find FLAG_LEDGER_INTERVAL (i.e. 256) previous ledger hashes + auto const hashIndex = prevLedger->read(keylet::skip()); + if (!hashIndex || !hashIndex->isFieldPresent(sfHashes)) + { + JLOG(j_.debug()) << "N-UNL: ledger " << seq << " no history."; + return {}; + } + auto const ledgerAncestors = hashIndex->getFieldV256(sfHashes).value(); + auto const numAncestors = ledgerAncestors.size(); + if (numAncestors < FLAG_LEDGER_INTERVAL) + { + JLOG(j_.debug()) << "N-UNL: ledger " << seq + << " not enough history. Can trace back only " + << numAncestors << " ledgers."; + return {}; + } + + // have enough ledger ancestors, build the score table + hash_map scoreTable; + for (auto const& k : unl) + { + scoreTable[k] = 0; + } + + // Query the validation container for every ledger hash and fill + // the score table. + for (int i = 0; i < FLAG_LEDGER_INTERVAL; ++i) + { + for (auto const& v : validations.getTrustedForLedger( + ledgerAncestors[numAncestors - 1 - i])) + { + if (scoreTable.count(v->getNodeID())) + ++scoreTable[v->getNodeID()]; + } + } + + // Return false if the validation message history or local node's + // participation in the history is not good. + auto const myValidationCount = [&]() -> std::uint32_t { + if (auto const it = scoreTable.find(myId_); it != scoreTable.end()) + return it->second; + return 0; + }(); + if (myValidationCount < negativeUnlMinLocalValsToVote) + { + JLOG(j_.debug()) << "N-UNL: ledger " << seq + << ". Local node only issued " << myValidationCount + << " validations in last " << FLAG_LEDGER_INTERVAL + << " ledgers." + << " The reliability measurement could be wrong."; + return {}; + } + else if ( + myValidationCount > negativeUnlMinLocalValsToVote && + myValidationCount <= FLAG_LEDGER_INTERVAL) + { + return scoreTable; + } + else + { + // cannot happen because validations.getTrustedForLedger does not + // return multiple validations of the same ledger from a validator. + JLOG(j_.error()) << "N-UNL: ledger " << seq << ". Local node issued " + << myValidationCount << " validations in last " + << FLAG_LEDGER_INTERVAL << " ledgers. Too many!"; + return {}; + } +} + +NegativeUNLVote::Candidates const +NegativeUNLVote::findAllCandidates( + hash_set const& unl, + hash_set const& negUnl, + hash_map const& scoreTable) +{ + // Compute if need to find more validators to disable + auto const canAdd = [&]() -> bool { + auto const maxNegativeListed = static_cast( + std::ceil(unl.size() * negativeUnlMaxListed)); + std::size_t negativeListed = 0; + for (auto const& n : unl) + { + if (negUnl.count(n)) + ++negativeListed; + } + bool const result = negativeListed < maxNegativeListed; + JLOG(j_.trace()) << "N-UNL: nodeId " << myId_ << " lowWaterMark " + << negativeUnlLowWaterMark << " highWaterMark " + << negativeUnlHighWaterMark << " canAdd " << result + << " negativeListed " << negativeListed + << " maxNegativeListed " << maxNegativeListed; + return result; + }(); + + Candidates candidates; + for (auto const& [nodeId, score] : scoreTable) + { + JLOG(j_.trace()) << "N-UNL: node " << nodeId << " score " << score; + + // Find toDisable Candidates: check if + // (1) canAdd, + // (2) has less than negativeUnlLowWaterMark validations, + // (3) is not in negUnl, and + // (4) is not a new validator. + if (canAdd && score < negativeUnlLowWaterMark && + !negUnl.count(nodeId) && !newValidators_.count(nodeId)) + { + JLOG(j_.trace()) << "N-UNL: toDisable candidate " << nodeId; + candidates.toDisableCandidates.push_back(nodeId); + } + + // Find toReEnable Candidates: check if + // (1) has more than negativeUnlHighWaterMark validations, + // (2) is in negUnl + if (score > negativeUnlHighWaterMark && negUnl.count(nodeId)) + { + JLOG(j_.trace()) << "N-UNL: toReEnable candidate " << nodeId; + candidates.toReEnableCandidates.push_back(nodeId); + } + } + + // If a negative UNL validator is removed from nodes' UNLs, it is no longer + // a validator. It should be removed from the negative UNL too. + // Note that even if it is still offline and in minority nodes' UNLs, it + // will not be re-added to the negative UNL. Because the UNLModify Tx will + // not be included in the agreed TxSet of a ledger. + // + // Find this kind of toReEnable Candidate if did not find any toReEnable + // candidate yet: check if + // (1) is in negUnl + // (2) is not in unl. + if (candidates.toReEnableCandidates.empty()) + { + for (auto const& n : negUnl) + { + if (!unl.count(n)) + { + candidates.toReEnableCandidates.push_back(n); + } + } + } + return candidates; +} + +void +NegativeUNLVote::newValidators( + LedgerIndex seq, + hash_set const& nowTrusted) +{ + std::lock_guard lock(mutex_); + for (auto const& n : nowTrusted) + { + if (newValidators_.find(n) == newValidators_.end()) + { + JLOG(j_.trace()) << "N-UNL: add a new validator " << n + << " at ledger seq=" << seq; + newValidators_[n] = seq; + } + } +} + +void +NegativeUNLVote::purgeNewValidators(LedgerIndex seq) +{ + std::lock_guard lock(mutex_); + auto i = newValidators_.begin(); + while (i != newValidators_.end()) + { + if (seq - i->second > newValidatorDisableSkip) + { + i = newValidators_.erase(i); + } + else + { + ++i; + } + } +} + +} // namespace ripple diff --git a/src/ripple/app/misc/NegativeUNLVote.h b/src/ripple/app/misc/NegativeUNLVote.h new file mode 100644 index 0000000000..da7bc5392b --- /dev/null +++ b/src/ripple/app/misc/NegativeUNLVote.h @@ -0,0 +1,217 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_NEGATIVEUNLVOTE_H_INCLUDED +#define RIPPLE_APP_MISC_NEGATIVEUNLVOTE_H_INCLUDED + +#include +#include +#include +#include +#include + +#include + +namespace ripple { + +template +class Validations; +class RCLValidationsAdaptor; +using RCLValidations = Validations; +class SHAMap; +namespace test { +class NegativeUNLVoteInternal_test; +class NegativeUNLVoteScoreTable_test; +} // namespace test + +/** + * Manager to create NegativeUNL votes. + */ +class NegativeUNLVote final +{ +public: + /** + * A validator is considered unreliable if its validations is less than + * negativeUnlLowWaterMark in the last flag ledger period. + * An unreliable validator is a candidate to be disabled by the NegativeUNL + * protocol. + */ + static constexpr size_t negativeUnlLowWaterMark = + FLAG_LEDGER_INTERVAL * 50 / 100; + /** + * An unreliable validator must have more than negativeUnlHighWaterMark + * validations in the last flag ledger period to be re-enabled. + */ + static constexpr size_t negativeUnlHighWaterMark = + FLAG_LEDGER_INTERVAL * 80 / 100; + /** + * The minimum number of validations of the local node for it to + * participate in the voting. + */ + static constexpr size_t negativeUnlMinLocalValsToVote = + FLAG_LEDGER_INTERVAL * 90 / 100; + /** + * We don't want to disable new validators immediately after adding them. + * So we skip voting for disabling them for 2 flag ledgers. + */ + static constexpr size_t newValidatorDisableSkip = FLAG_LEDGER_INTERVAL * 2; + /** + * We only want to put 25% of the UNL on the NegativeUNL. + */ + static constexpr float negativeUnlMaxListed = 0.25; + + /** + * A flag indicating whether a UNLModify Tx is to disable or to re-enable + * a validator. + */ + enum NegativeUNLModify { + ToDisable, // UNLModify Tx is to disable a validator + ToReEnable // UNLModify Tx is to re-enable a validator + }; + + /** + * Constructor + * + * @param myId the NodeID of the local node + * @param j log + */ + NegativeUNLVote(NodeID const& myId, beast::Journal j); + ~NegativeUNLVote() = default; + + /** + * Cast our local vote on the NegativeUNL candidates. + * + * @param prevLedger the parent ledger + * @param unlKeys the trusted master keys of validators in the UNL + * @param validations the validation message container + * @note validations is an in/out parameter. It contains validation messages + * that will be deleted when no longer needed by other consensus logic. This + * function asks it to keep the validation messages long enough for this + * function to use. + * @param initialSet the transactions set for adding ttUNL_MODIFY Tx if any + */ + void + doVoting( + std::shared_ptr const& prevLedger, + hash_set const& unlKeys, + RCLValidations& validations, + std::shared_ptr const& initialSet); + + /** + * Notify NegativeUNLVote that new validators are added. + * So that they don't get voted to the NegativeUNL immediately. + * + * @param seq the current LedgerIndex when adding the new validators + * @param nowTrusted the new validators + */ + void + newValidators(LedgerIndex seq, hash_set const& nowTrusted); + +private: + NodeID const myId_; + beast::Journal j_; + mutable std::mutex mutex_; + hash_map newValidators_; + + /** + * UNLModify Tx candidates + */ + struct Candidates + { + std::vector toDisableCandidates; + std::vector toReEnableCandidates; + }; + + /** + * Add a ttUNL_MODIFY Tx to the transaction set. + * + * @param seq the LedgerIndex when adding the Tx + * @param vp the master public key of the validator + * @param modify disabling or re-enabling the validator + * @param initialSet the transaction set + */ + void + addTx( + LedgerIndex seq, + PublicKey const& vp, + NegativeUNLModify modify, + std::shared_ptr const& initialSet); + + /** + * Pick one candidate from a vector of candidates. + * + * @param randomPadData the data used for picking a candidate. + * @note Nodes must use the same randomPadData for picking the same + * candidate. The hash of the parent ledger is used. + * @param candidates the vector of candidates + * @return the picked candidate + */ + NodeID + choose(uint256 const& randomPadData, std::vector const& candidates); + + /** + * Build a reliability measurement score table of validators' validation + * messages in the last flag ledger period. + * + * @param prevLedger the parent ledger + * @param unl the trusted master keys + * @param validations the validation container + * @note validations is an in/out parameter. It contains validation messages + * that will be deleted when no longer needed by other consensus logic. This + * function asks it to keep the validation messages long enough for this + * function to use. + * @return the built scoreTable or empty optional if table could not be + * built + */ + std::optional> + buildScoreTable( + std::shared_ptr const& prevLedger, + hash_set const& unl, + RCLValidations& validations); + + /** + * Process the score table and find all disabling and re-enabling + * candidates. + * + * @param unl the trusted master keys + * @param negUnl the NegativeUNL + * @param scoreTable the score table + * @return the candidates to disable and the candidates to re-enable + */ + Candidates const + findAllCandidates( + hash_set const& unl, + hash_set const& negUnl, + hash_map const& scoreTable); + + /** + * Purge validators that are not new anymore. + * + * @param seq the current LedgerIndex + */ + void + purgeNewValidators(LedgerIndex seq); + + friend class test::NegativeUNLVoteInternal_test; + friend class test::NegativeUNLVoteScoreTable_test; +}; + +} // namespace ripple + +#endif diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 0dc0771d24..6a19cc66bb 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -56,6 +56,7 @@ #include #include #include +#include #include #include #include @@ -1749,6 +1750,8 @@ NetworkOPsImp::beginConsensus(uint256 const& networkClosed) closingInfo.parentHash == m_ledgerMaster.getClosedLedger()->info().hash); + if (prevLedger->rules().enabled(featureNegativeUNL)) + app_.validators().setNegativeUnl(prevLedger->negativeUnl()); TrustChanges const changes = app_.validators().updateTrusted( app_.getValidations().getCurrentNodeIDs()); @@ -1759,7 +1762,8 @@ NetworkOPsImp::beginConsensus(uint256 const& networkClosed) app_.timeKeeper().closeTime(), networkClosed, prevLedger, - changes.removed); + changes.removed, + changes.added); const ConsensusPhase currPhase = mConsensus.phase(); if (mLastConsensusPhase != currPhase) diff --git a/src/ripple/app/misc/ValidatorList.h b/src/ripple/app/misc/ValidatorList.h index c47119eb75..cf0ca00fe9 100644 --- a/src/ripple/app/misc/ValidatorList.h +++ b/src/ripple/app/misc/ValidatorList.h @@ -38,6 +38,7 @@ namespace ripple { // predeclaration class Overlay; class HashRouter; +class STValidation; enum class ListDisposition { /// List is valid @@ -159,6 +160,9 @@ class ValidatorList PublicKey localPubKey_; + // The master public keys of the current negative UNL + hash_set negativeUnl_; + // Currently supported version of publisher list format static constexpr std::uint32_t requiredListVersion = 1; static const std::string filePrefix_; @@ -505,6 +509,37 @@ public: return {quorum_, trustedSigningKeys_}; } + /** + * get the trusted master public keys + * @return the public keys + */ + hash_set + getTrustedMasterKeys() const; + + /** + * get the master public keys of Negative UNL validators + * @return the master public keys + */ + hash_set + getNegativeUnl() const; + + /** + * set the Negative UNL with validators' master public keys + * @param negUnl the public keys + */ + void + setNegativeUnl(hash_set const& negUnl); + + /** + * Remove validations that are from validators on the negative UNL. + * + * @param validations the validations to filter + * @return a filtered copy of the validations + */ + std::vector> + negativeUNLFilter( + std::vector>&& validations) const; + private: /** Get the filename used for caching UNLs */ @@ -547,12 +582,19 @@ private: /** Return quorum for trusted validator set - @param trusted Number of trusted validator keys + @param unlSize Number of trusted validator keys - @param seen Number of trusted validators that have signed - recently received validations */ + @param effectiveUnlSize Number of trusted validator keys that are not in + the NegativeUNL + + @param seenSize Number of trusted validators that have signed + recently received validations + */ std::size_t - calculateQuorum(std::size_t trusted, std::size_t seen); + calculateQuorum( + std::size_t unlSize, + std::size_t effectiveUnlSize, + std::size_t seenSize); }; } // namespace ripple diff --git a/src/ripple/app/misc/impl/ValidatorList.cpp b/src/ripple/app/misc/impl/ValidatorList.cpp index 710ad34928..ebb34a7e52 100644 --- a/src/ripple/app/misc/impl/ValidatorList.cpp +++ b/src/ripple/app/misc/impl/ValidatorList.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -746,6 +747,16 @@ ValidatorList::getJson() const } }); + // Negative UNL + if (!negativeUnl_.empty()) + { + Json::Value& jNegativeUNL = (res[jss::NegativeUNL] = Json::arrayValue); + for (auto const& k : negativeUnl_) + { + jNegativeUNL.append(toBase58(TokenType::NodePublic, k)); + } + } + return res; } @@ -818,7 +829,10 @@ ValidatorList::getAvailable(boost::beast::string_view const& pubKey) } std::size_t -ValidatorList::calculateQuorum(std::size_t trusted, std::size_t seen) +ValidatorList::calculateQuorum( + std::size_t unlSize, + std::size_t effectiveUnlSize, + std::size_t seenSize) { // Do not use achievable quorum until lists from all configured // publishers are available @@ -858,11 +872,16 @@ ValidatorList::calculateQuorum(std::size_t trusted, std::size_t seen) // Oi,j > nj/2 + ni − qi + ti,j // ni - pi > (ni - pi + pj)/2 + ni − .8*ni + .2*ni // pi + pj < .2*ni - auto quorum = static_cast(std::ceil(trusted * 0.8f)); + // + // Note that the negative UNL protocol introduced the AbsoluteMinimumQuorum + // which is 60% of the original UNL size. The effective quorum should + // not be lower than it. + auto quorum = static_cast(std::max( + std::ceil(effectiveUnlSize * 0.8f), std::ceil(unlSize * 0.6f))); // Use lower quorum specified via command line if the normal quorum appears // unreachable based on the number of recently received validations. - if (minimumQuorum_ && *minimumQuorum_ < quorum && seen < quorum) + if (minimumQuorum_ && *minimumQuorum_ < quorum && seenSize < quorum) { quorum = *minimumQuorum_; @@ -922,7 +941,28 @@ ValidatorList::updateTrusted(hash_set const& seenValidators) << trustedMasterKeys_.size() << " of " << keyListings_.size() << " listed validators eligible for inclusion in the trusted set"; - quorum_ = calculateQuorum(trustedMasterKeys_.size(), seenValidators.size()); + auto unlSize = trustedMasterKeys_.size(); + auto effectiveUnlSize = unlSize; + auto seenSize = seenValidators.size(); + if (!negativeUnl_.empty()) + { + for (auto const& k : trustedMasterKeys_) + { + if (negativeUnl_.count(k)) + --effectiveUnlSize; + } + hash_set negUnlNodeIDs; + for (auto const& k : negativeUnl_) + { + negUnlNodeIDs.emplace(calcNodeID(k)); + } + for (auto const& nid : seenValidators) + { + if (negUnlNodeIDs.count(nid)) + --seenSize; + } + } + quorum_ = calculateQuorum(unlSize, effectiveUnlSize, seenSize); JLOG(j_.debug()) << "Using quorum of " << quorum_ << " for new set of " << trustedMasterKeys_.size() << " trusted validators (" @@ -939,4 +979,57 @@ ValidatorList::updateTrusted(hash_set const& seenValidators) return trustChanges; } +hash_set +ValidatorList::getTrustedMasterKeys() const +{ + std::shared_lock lock{mutex_}; + return trustedMasterKeys_; +} + +hash_set +ValidatorList::getNegativeUnl() const +{ + std::shared_lock lock{mutex_}; + return negativeUnl_; +} + +void +ValidatorList::setNegativeUnl(hash_set const& negUnl) +{ + std::lock_guard lock{mutex_}; + negativeUnl_ = negUnl; +} + +std::vector> +ValidatorList::negativeUNLFilter( + std::vector>&& validations) const +{ + // Remove validations that are from validators on the negative UNL. + auto ret = std::move(validations); + + std::shared_lock lock{mutex_}; + if (!negativeUnl_.empty()) + { + ret.erase( + std::remove_if( + ret.begin(), + ret.end(), + [&](auto const& v) -> bool { + if (auto const masterKey = + getTrustedKey(v->getSignerPublic()); + masterKey) + { + return negativeUnl_.count(*masterKey); + } + else + { + return false; + } + }), + ret.end()); + } + + return ret; +} + } // namespace ripple diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index a75962b001..9563790d86 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -17,11 +17,13 @@ */ //============================================================================== +#include #include #include #include #include #include +#include #include #include @@ -62,6 +64,13 @@ Change::preflight(PreflightContext const& ctx) return temBAD_SEQUENCE; } + if (ctx.tx.getTxnType() == ttUNL_MODIFY && + !ctx.rules.enabled(featureNegativeUNL)) + { + JLOG(ctx.j.warn()) << "Change: NegativeUNL not enabled"; + return temDISABLED; + } + return tesSUCCESS; } @@ -76,20 +85,32 @@ Change::preclaim(PreclaimContext const& ctx) return temINVALID; } - if (ctx.tx.getTxnType() != ttAMENDMENT && ctx.tx.getTxnType() != ttFEE) - return temUNKNOWN; - - return tesSUCCESS; + switch (ctx.tx.getTxnType()) + { + case ttAMENDMENT: + case ttFEE: + case ttUNL_MODIFY: + return tesSUCCESS; + default: + return temUNKNOWN; + } } TER Change::doApply() { - if (ctx_.tx.getTxnType() == ttAMENDMENT) - return applyAmendment(); - - assert(ctx_.tx.getTxnType() == ttFEE); - return applyFee(); + switch (ctx_.tx.getTxnType()) + { + case ttAMENDMENT: + return applyAmendment(); + case ttFEE: + return applyFee(); + case ttUNL_MODIFY: + return applyUNLModify(); + default: + assert(0); + return tefFAILURE; + } } void @@ -221,4 +242,130 @@ Change::applyFee() return tesSUCCESS; } +TER +Change::applyUNLModify() +{ + if (!isFlagLedger(view().seq())) + { + JLOG(j_.warn()) << "N-UNL: applyUNLModify, not a flag ledger, seq=" + << view().seq(); + return tefFAILURE; + } + + if (!ctx_.tx.isFieldPresent(sfUNLModifyDisabling) || + ctx_.tx.getFieldU8(sfUNLModifyDisabling) > 1 || + !ctx_.tx.isFieldPresent(sfLedgerSequence) || + !ctx_.tx.isFieldPresent(sfUNLModifyValidator)) + { + JLOG(j_.warn()) << "N-UNL: applyUNLModify, wrong Tx format."; + return tefFAILURE; + } + + bool const disabling = ctx_.tx.getFieldU8(sfUNLModifyDisabling); + auto const seq = ctx_.tx.getFieldU32(sfLedgerSequence); + if (seq != view().seq()) + { + JLOG(j_.warn()) << "N-UNL: applyUNLModify, wrong ledger seq=" << seq; + return tefFAILURE; + } + + Blob const validator = ctx_.tx.getFieldVL(sfUNLModifyValidator); + if (!publicKeyType(makeSlice(validator))) + { + JLOG(j_.warn()) << "N-UNL: applyUNLModify, bad validator key"; + return tefFAILURE; + } + + JLOG(j_.info()) << "N-UNL: applyUNLModify, " + << (disabling ? "ToDisable" : "ToReEnable") + << " seq=" << seq + << " validator data:" << strHex(validator); + + auto const k = keylet::negativeUNL(); + SLE::pointer negUnlObject = view().peek(k); + if (!negUnlObject) + { + negUnlObject = std::make_shared(k); + view().insert(negUnlObject); + } + + bool const found = [&] { + if (negUnlObject->isFieldPresent(sfNegativeUNL)) + { + auto const& negUnl = negUnlObject->getFieldArray(sfNegativeUNL); + for (auto const& v : negUnl) + { + if (v.isFieldPresent(sfPublicKey) && + v.getFieldVL(sfPublicKey) == validator) + return true; + } + } + return false; + }(); + + if (disabling) + { + // cannot have more than one toDisable + if (negUnlObject->isFieldPresent(sfNegativeUNLToDisable)) + { + JLOG(j_.warn()) << "N-UNL: applyUNLModify, already has ToDisable"; + return tefFAILURE; + } + + // cannot be the same as toReEnable + if (negUnlObject->isFieldPresent(sfNegativeUNLToReEnable)) + { + if (negUnlObject->getFieldVL(sfNegativeUNLToReEnable) == validator) + { + JLOG(j_.warn()) + << "N-UNL: applyUNLModify, ToDisable is same as ToReEnable"; + return tefFAILURE; + } + } + + // cannot be in negative UNL already + if (found) + { + JLOG(j_.warn()) + << "N-UNL: applyUNLModify, ToDisable already in negative UNL"; + return tefFAILURE; + } + + negUnlObject->setFieldVL(sfNegativeUNLToDisable, validator); + } + else + { + // cannot have more than one toReEnable + if (negUnlObject->isFieldPresent(sfNegativeUNLToReEnable)) + { + JLOG(j_.warn()) << "N-UNL: applyUNLModify, already has ToReEnable"; + return tefFAILURE; + } + + // cannot be the same as toDisable + if (negUnlObject->isFieldPresent(sfNegativeUNLToDisable)) + { + if (negUnlObject->getFieldVL(sfNegativeUNLToDisable) == validator) + { + JLOG(j_.warn()) + << "N-UNL: applyUNLModify, ToReEnable is same as ToDisable"; + return tefFAILURE; + } + } + + // must be in negative UNL + if (!found) + { + JLOG(j_.warn()) + << "N-UNL: applyUNLModify, ToReEnable is not in negative UNL"; + return tefFAILURE; + } + + negUnlObject->setFieldVL(sfNegativeUNLToReEnable, validator); + } + + view().update(negUnlObject); + return tesSUCCESS; +} + } // namespace ripple diff --git a/src/ripple/app/tx/impl/Change.h b/src/ripple/app/tx/impl/Change.h index 54e0aa4f92..9f79278411 100644 --- a/src/ripple/app/tx/impl/Change.h +++ b/src/ripple/app/tx/impl/Change.h @@ -59,6 +59,9 @@ private: TER applyFee(); + + TER + applyUNLModify(); }; } // namespace ripple diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 34e9d90ec7..73b20a0f1d 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -365,6 +365,7 @@ LedgerEntryTypesMatch::visitEntry( case ltPAYCHAN: case ltCHECK: case ltDEPOSIT_PREAUTH: + case ltNEGATIVE_UNL: break; default: invalidTypeAdded_ = true; diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index fddf922165..8ebfd6d3c7 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -86,6 +86,7 @@ invoke_preflight(PreflightContext const& ctx) return DeleteAccount ::preflight(ctx); case ttAMENDMENT: case ttFEE: + case ttUNL_MODIFY: return Change ::preflight(ctx); default: assert(false); @@ -173,6 +174,7 @@ invoke_preclaim(PreclaimContext const& ctx) return invoke_preclaim(ctx); case ttAMENDMENT: case ttFEE: + case ttUNL_MODIFY: return invoke_preclaim(ctx); default: assert(false); @@ -227,6 +229,7 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) return DeleteAccount::calculateBaseFee(view, tx); case ttAMENDMENT: case ttFEE: + case ttUNL_MODIFY: return Change::calculateBaseFee(view, tx); default: assert(false); @@ -294,6 +297,7 @@ invoke_calculateConsequences(STTx const& tx) return invoke_calculateConsequences(tx); case ttAMENDMENT: case ttFEE: + case ttUNL_MODIFY: [[fallthrough]]; default: assert(false); @@ -390,7 +394,8 @@ invoke_apply(ApplyContext& ctx) return p(); } case ttAMENDMENT: - case ttFEE: { + case ttFEE: + case ttUNL_MODIFY: { Change p(ctx); return p(); } diff --git a/src/ripple/consensus/Validations.h b/src/ripple/consensus/Validations.h index 8626d68061..d4df679bf9 100644 --- a/src/ripple/consensus/Validations.h +++ b/src/ripple/consensus/Validations.h @@ -322,6 +322,9 @@ class Validations beast::uhash<>> bySequence_; + // Sequence of the earliest validation to keep from expire + boost::optional toKeep_; + // Represents the ancestry of validated ledgers LedgerTrie trie_; @@ -686,15 +689,47 @@ public: return ValStatus::current; } + /** + * Set the smallest sequence number of validations to keep from expire + * @param s the sequence number + */ + void + setSeqToKeep(Seq const& s) + { + std::lock_guard lock{mutex_}; + toKeep_ = s; + } + /** Expire old validation sets Remove validation sets that were accessed more than - validationSET_EXPIRES ago. + validationSET_EXPIRES ago and were not asked to keep. */ void expire() { std::lock_guard lock{mutex_}; + if (toKeep_) + { + for (auto i = byLedger_.begin(); i != byLedger_.end(); ++i) + { + auto const& validationMap = i->second; + if (!validationMap.empty() && + validationMap.begin()->second.seq() >= toKeep_) + { + byLedger_.touch(i); + } + } + + for (auto i = bySequence_.begin(); i != bySequence_.end(); ++i) + { + if (i->first >= toKeep_) + { + bySequence_.touch(i); + } + } + } + beast::expire(byLedger_, parms_.validationSET_EXPIRES); beast::expire(bySequence_, parms_.validationSET_EXPIRES); } diff --git a/src/ripple/proto/org/xrpl/rpc/v1/common.proto b/src/ripple/proto/org/xrpl/rpc/v1/common.proto index 3cc3c73ae4..cc0a0aa14b 100644 --- a/src/ripple/proto/org/xrpl/rpc/v1/common.proto +++ b/src/ripple/proto/org/xrpl/rpc/v1/common.proto @@ -7,7 +7,7 @@ option java_multiple_files = true; import "org/xrpl/rpc/v1/amount.proto"; import "org/xrpl/rpc/v1/account.proto"; -// These fields are used in many different messsage types. They can be present +// These fields are used in many different message types. They can be present // in one or more transactions, as well as metadata of one or more transactions. // Each is defined as its own message type with a single field "value", to // ensure the field is the correct type everywhere it's used @@ -70,6 +70,11 @@ message HighQualityOut uint32 value = 1; } +message FirstLedgerSequence +{ + uint32 value = 1; +} + message LastLedgerSequence { uint32 value = 1; @@ -351,6 +356,15 @@ message TransactionSignature bytes value = 1; } +message NegativeUnlToDisable +{ + bytes value = 1; +} + +message NegativeUnlToReEnable +{ + bytes value = 1; +} // *** Messages wrapping a Currency value *** @@ -474,3 +488,12 @@ message SignerEntry SignerWeight signer_weight = 2; } + +// Next field: 3 +message NegativeUnlEntry +{ + PublicKey public_key = 1; + + FirstLedgerSequence ledger_sequence = 2; +} + diff --git a/src/ripple/proto/org/xrpl/rpc/v1/ledger_objects.proto b/src/ripple/proto/org/xrpl/rpc/v1/ledger_objects.proto index 9bcaa2672d..2ad820dd2f 100644 --- a/src/ripple/proto/org/xrpl/rpc/v1/ledger_objects.proto +++ b/src/ripple/proto/org/xrpl/rpc/v1/ledger_objects.proto @@ -6,7 +6,7 @@ option java_multiple_files = true; import "org/xrpl/rpc/v1/common.proto"; -// Next field: 13 +// Next field: 14 message LedgerObject { oneof object @@ -23,10 +23,11 @@ message LedgerObject PayChannel pay_channel = 10; RippleState ripple_state = 11; SignerList signer_list = 12; + NegativeUnl negative_unl = 13; } } -// Next field: 13 +// Next field: 14 enum LedgerEntryType { LEDGER_ENTRY_TYPE_UNSPECIFIED = 0; @@ -42,6 +43,7 @@ enum LedgerEntryType LEDGER_ENTRY_TYPE_PAY_CHANNEL = 10; LEDGER_ENTRY_TYPE_RIPPLE_STATE = 11; LEDGER_ENTRY_TYPE_SIGNER_LIST = 12; + LEDGER_ENTRY_TYPE_NEGATIVE_UNL = 13; } // Next field: 15 @@ -329,3 +331,13 @@ message SignerList SignerQuorum signer_quorum = 7; } + +// Next field: 4 +message NegativeUnl +{ + repeated NegativeUnlEntry negative_unl_entries = 1; + + NegativeUnlToDisable validator_to_disable = 2; + + NegativeUnlToReEnable validator_to_re_enable = 3; +} \ No newline at end of file diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index e378ec49ee..bec4ad9bdd 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -112,8 +112,8 @@ class FeatureCollections "fix1781", // XRPEndpointSteps should be included in the circular // payment check "HardenedValidations", - "fixAmendmentMajorityCalc"}; // Fix Amendment majority calculation - + "fixAmendmentMajorityCalc", // Fix Amendment majority calculation + "NegativeUNL"}; std::vector features; boost::container::flat_map featureToIndex; boost::container::flat_map nameToFeature; @@ -368,6 +368,7 @@ extern uint256 const featureRequireFullyCanonicalSig; extern uint256 const fix1781; extern uint256 const featureHardenedValidations; extern uint256 const fixAmendmentMajorityCalc; +extern uint256 const featureNegativeUNL; } // namespace ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 95c52805a6..1cbb8fd56c 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -85,6 +85,10 @@ skip(LedgerIndex ledger) noexcept; Keylet const& fees() noexcept; +/** The (fixed) index of the object containing the ledger negativeUnl. */ +Keylet const& +negativeUNL() noexcept; + /** The beginning of an order book */ struct book_t { diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index bee6a467ad..18b0170081 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -86,6 +86,8 @@ enum LedgerEntryType { ltDEPOSIT_PREAUTH = 'p', + ltNEGATIVE_UNL = 'N', + // No longer used or supported. Left here to prevent accidental // reassignment of the ledger type. ltNICKNAME [[deprecated]] = 'n', diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index abf7037e7d..e0f78d5ec5 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -340,6 +340,7 @@ extern SF_U8 const sfCloseResolution; extern SF_U8 const sfMethod; extern SF_U8 const sfTransactionResult; extern SF_U8 const sfTickSize; +extern SF_U8 const sfUNLModifyDisabling; // 16-bit integers extern SF_U16 const sfLedgerEntryType; @@ -375,7 +376,7 @@ extern SF_U32 const sfStampEscrow; extern SF_U32 const sfBondAmount; extern SF_U32 const sfLoadFee; extern SF_U32 const sfOfferSequence; -extern SF_U32 const sfFirstLedgerSequence; // Deprecated: do not use +extern SF_U32 const sfFirstLedgerSequence; extern SF_U32 const sfLastLedgerSequence; extern SF_U32 const sfTransactionIndex; extern SF_U32 const sfOperationLimit; @@ -471,6 +472,9 @@ extern SF_Blob const sfMemoFormat; extern SF_Blob const sfFulfillment; extern SF_Blob const sfCondition; extern SF_Blob const sfMasterSignature; +extern SF_Blob const sfUNLModifyValidator; +extern SF_Blob const sfNegativeUNLToDisable; +extern SF_Blob const sfNegativeUNLToReEnable; // account extern SF_Account const sfAccount; @@ -504,6 +508,7 @@ extern SField const sfMemo; extern SField const sfSignerEntry; extern SField const sfSigner; extern SField const sfMajority; +extern SField const sfNegativeUNLEntry; // array of objects // ARRAY/1 is reserved for end of array @@ -516,7 +521,7 @@ extern SField const sfSufficient; extern SField const sfAffectedNodes; extern SField const sfMemos; extern SField const sfMajorities; - +extern SField const sfNegativeUNL; //------------------------------------------------------------------------------ } // namespace ripple diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index ad4e517426..5ff3cf3ee6 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -58,6 +58,7 @@ enum TxType { ttAMENDMENT = 100, ttFEE = 101, + ttUNL_MODIFY = 102, }; /** Manages the list of known transaction formats. diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index b9c9567393..6a0c34c6ce 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -131,7 +131,9 @@ detail::supportedAmendments() "RequireFullyCanonicalSig", "fix1781", "HardenedValidations", - "fixAmendmentMajorityCalc"}; + "fixAmendmentMajorityCalc", + //"NegativeUNL" // Commented out to prevent automatic enablement + }; return supported; } @@ -182,7 +184,8 @@ uint256 const featureRequireFullyCanonicalSig = *getRegisteredFeature("RequireFullyCanonicalSig"), fix1781 = *getRegisteredFeature("fix1781"), featureHardenedValidations = *getRegisteredFeature("HardenedValidations"), - fixAmendmentMajorityCalc = *getRegisteredFeature("fixAmendmentMajorityCalc"); + fixAmendmentMajorityCalc = *getRegisteredFeature("fixAmendmentMajorityCalc"), + featureNegativeUNL = *getRegisteredFeature("NegativeUNL"); // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 6df08f90d2..acb3ef14e6 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -58,6 +58,7 @@ enum class LedgerNameSpace : std::uint16_t { XRP_PAYMENT_CHANNEL = 'x', CHECK = 'C', DEPOSIT_PREAUTH = 'p', + NEGATIVE_UNL = 'N', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -162,6 +163,14 @@ fees() noexcept return ret; } +Keylet const& +negativeUNL() noexcept +{ + static Keylet const ret{ + ltNEGATIVE_UNL, indexHash(LedgerNameSpace::NEGATIVE_UNL)}; + return ret; +} + Keylet book_t::operator()(Book const& b) const { diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 199c1265e1..0c0abdbb6c 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -224,6 +224,15 @@ LedgerFormats::LedgerFormats() {sfPreviousTxnLgrSeq, soeREQUIRED}, }, commonFields); + + add(jss::NegativeUNL, + ltNEGATIVE_UNL, + { + {sfNegativeUNL, soeOPTIONAL}, + {sfNegativeUNLToDisable, soeOPTIONAL}, + {sfNegativeUNLToReEnable, soeOPTIONAL}, + }, + commonFields); } LedgerFormats const& diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 4784d2de99..558635bee5 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -56,6 +56,7 @@ SF_U8 const sfTransactionResult(access, STI_UINT8, 3, "TransactionResult"); // 8-bit integers (uncommon) SF_U8 const sfTickSize(access, STI_UINT8, 16, "TickSize"); +SF_U8 const sfUNLModifyDisabling(access, STI_UINT8, 17, "UNLModifyDisabling"); // 16-bit integers SF_U16 const sfLedgerEntryType( @@ -101,11 +102,8 @@ SF_U32 const sfStampEscrow(access, STI_UINT32, 22, "StampEscrow"); SF_U32 const sfBondAmount(access, STI_UINT32, 23, "BondAmount"); SF_U32 const sfLoadFee(access, STI_UINT32, 24, "LoadFee"); SF_U32 const sfOfferSequence(access, STI_UINT32, 25, "OfferSequence"); -SF_U32 const sfFirstLedgerSequence( - access, - STI_UINT32, - 26, - "FirstLedgerSequence"); // Deprecated: do not use +SF_U32 const + sfFirstLedgerSequence(access, STI_UINT32, 26, "FirstLedgerSequence"); SF_U32 const sfLastLedgerSequence(access, STI_UINT32, 27, "LastLedgerSequence"); SF_U32 const sfTransactionIndex(access, STI_UINT32, 28, "TransactionIndex"); SF_U32 const sfOperationLimit(access, STI_UINT32, 29, "OperationLimit"); @@ -225,6 +223,10 @@ SF_Blob const sfMasterSignature( "MasterSignature", SField::sMD_Default, SField::notSigning); +SF_Blob const sfUNLModifyValidator(access, STI_VL, 19, "UNLModifyValidator"); +SF_Blob const sfNegativeUNLToDisable(access, STI_VL, 20, "ValidatorToDisable"); +SF_Blob const + sfNegativeUNLToReEnable(access, STI_VL, 21, "ValidatorToReEnable"); // account SF_Account const sfAccount(access, STI_ACCOUNT, 1, "Account"); @@ -263,6 +265,7 @@ SField const sfSignerEntry(access, STI_OBJECT, 11, "SignerEntry"); SField const sfSigner(access, STI_OBJECT, 16, "Signer"); // 17 has not been used yet... SField const sfMajority(access, STI_OBJECT, 18, "Majority"); +SField const sfNegativeUNLEntry(access, STI_OBJECT, 19, "DisabledValidator"); // array of objects // ARRAY/1 is reserved for end of array @@ -284,6 +287,7 @@ SField const sfMemos(access, STI_ARRAY, 9, "Memos"); // array of objects (uncommon) SField const sfMajorities(access, STI_ARRAY, 16, "Majorities"); +SField const sfNegativeUNL(access, STI_ARRAY, 17, "NegativeUNL"); SField::SField( private_access_tag_t, diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index 1da713e0f3..d5f468b66a 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -527,7 +527,7 @@ isPseudoTx(STObject const& tx) if (!t) return false; auto tt = safe_cast(*t); - return tt == ttAMENDMENT || tt == ttFEE; + return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY; } } // namespace ripple diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index bea1dc6018..24a8ef197b 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -152,6 +152,15 @@ TxFormats::TxFormats() }, commonFields); + add(jss::UNLModify, + ttUNL_MODIFY, + { + {sfUNLModifyDisabling, soeREQUIRED}, + {sfLedgerSequence, soeREQUIRED}, + {sfUNLModifyValidator, soeREQUIRED}, + }, + commonFields); + add(jss::TicketCreate, ttTICKET_CREATE, { diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 1544a5b6a7..1df4bf7fcf 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -82,6 +82,7 @@ JSS(PaymentChannelFund); // transaction type. JSS(RippleState); // ledger type. JSS(SLE_hit_rate); // out: GetCounts. JSS(SetFee); // transaction type. +JSS(UNLModify); // transaction type. JSS(SettleDelay); // in: TransactionSign JSS(SendMax); // in: TransactionSign JSS(Sequence); // in/out: TransactionSign; field. @@ -576,8 +577,8 @@ JSS(vote); // in: Feature JSS(warning); // rpc: JSS(warnings); // out: server_info, server_state JSS(workers); -JSS(write_load); // out: GetCounts - +JSS(write_load); // out: GetCounts +JSS(NegativeUNL); // out: ValidatorList; ledger type #undef JSS } // namespace jss diff --git a/src/ripple/rpc/impl/GRPCHelpers.cpp b/src/ripple/rpc/impl/GRPCHelpers.cpp index 0c068b08d4..7b68f9fe33 100644 --- a/src/ripple/rpc/impl/GRPCHelpers.cpp +++ b/src/ripple/rpc/impl/GRPCHelpers.cpp @@ -485,6 +485,36 @@ populateFlags(T& to, STObject const& from) [&to]() { return to.mutable_flags(); }, from, sfFlags); } +template +void +populateFirstLedgerSequence(T& to, STObject const& from) +{ + populateProtoPrimitive( + [&to]() { return to.mutable_ledger_sequence(); }, + from, + sfFirstLedgerSequence); +} + +template +void +populateNegativeUNLToDisable(T& to, STObject const& from) +{ + populateProtoPrimitive( + [&to]() { return to.mutable_validator_to_disable(); }, + from, + sfNegativeUNLToDisable); +} + +template +void +populateNegativeUNLToReEnable(T& to, STObject const& from) +{ + populateProtoPrimitive( + [&to]() { return to.mutable_validator_to_re_enable(); }, + from, + sfNegativeUNLToReEnable); +} + template void populateLastLedgerSequence(T& to, STObject const& from) @@ -846,6 +876,21 @@ populateSignerEntries(T& to, STObject const& from) sfSignerEntry); } +template +void +populateNegativeUNLEntries(T& to, STObject const& from) +{ + populateProtoArray( + [&to]() { return to.add_negative_unl_entries(); }, + [](auto& innerObj, auto& innerProto) { + populatePublicKey(innerProto, innerObj); + populateFirstLedgerSequence(innerProto, innerObj); + }, + from, + sfNegativeUNL, + sfNegativeUNLEntry); +} + template void populateMemos(T& to, STObject const& from) @@ -1417,6 +1462,16 @@ convert(org::xrpl::rpc::v1::SignerList& to, STObject const& from) populateSignerListID(to, from); } +void +convert(org::xrpl::rpc::v1::NegativeUnl& to, STObject const& from) +{ + populateNegativeUNLEntries(to, from); + + populateNegativeUNLToDisable(to, from); + + populateNegativeUNLToReEnable(to, from); +} + void setLedgerEntryType( org::xrpl::rpc::v1::AffectedNode& proto, @@ -1472,6 +1527,10 @@ setLedgerEntryType( proto.set_ledger_entry_type( org::xrpl::rpc::v1::LEDGER_ENTRY_TYPE_DEPOSIT_PREAUTH); break; + case ltNEGATIVE_UNL: + proto.set_ledger_entry_type( + org::xrpl::rpc::v1::LEDGER_ENTRY_TYPE_NEGATIVE_UNL); + break; } } @@ -1517,6 +1576,9 @@ convert(T& to, STObject& from, std::uint16_t type) case ltDEPOSIT_PREAUTH: RPC::convert(*to.mutable_deposit_preauth(), from); break; + case ltNEGATIVE_UNL: + RPC::convert(*to.mutable_negative_unl(), from); + break; } } diff --git a/src/ripple/rpc/impl/GRPCHelpers.h b/src/ripple/rpc/impl/GRPCHelpers.h index bc856431da..e9eae0107c 100644 --- a/src/ripple/rpc/impl/GRPCHelpers.h +++ b/src/ripple/rpc/impl/GRPCHelpers.h @@ -58,6 +58,9 @@ convert(org::xrpl::rpc::v1::AccountRoot& to, STObject const& from); void convert(org::xrpl::rpc::v1::SignerList& to, STObject const& from); +void +convert(org::xrpl::rpc::v1::NegativeUnl& to, STObject const& from); + template void convert(T& to, STAmount const& from) diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index 5bb6fb0ee2..cb069c2c79 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -1267,6 +1267,185 @@ private: } } + void + testNegativeUNL() + { + testcase("NegativeUNL"); + jtx::Env env(*this); + PublicKey emptyLocalKey; + ManifestCache manifests; + + auto createValidatorList = + [&](std::uint32_t vlSize, + boost::optional minimumQuorum = {}) + -> std::shared_ptr { + auto trustedKeys = std::make_shared( + manifests, + manifests, + env.timeKeeper(), + env.app().config().legacy("database_path"), + env.journal, + minimumQuorum); + + std::vector cfgPublishers; + std::vector cfgKeys; + hash_set activeValidators; + cfgKeys.reserve(vlSize); + while (cfgKeys.size() < cfgKeys.capacity()) + { + auto const valKey = randomNode(); + cfgKeys.push_back(toBase58(TokenType::NodePublic, valKey)); + activeValidators.emplace(calcNodeID(valKey)); + } + if (trustedKeys->load(emptyLocalKey, cfgKeys, cfgPublishers)) + { + trustedKeys->updateTrusted(activeValidators); + if (trustedKeys->quorum() == std::ceil(cfgKeys.size() * 0.8f)) + return trustedKeys; + } + return nullptr; + }; + + /* + * Test NegativeUNL + * == Combinations == + * -- UNL size: 34, 35, 57 + * -- nUNL size: 0%, 20%, 30%, 50% + * + * == with UNL size 60 + * -- set == get, + * -- check quorum, with nUNL size: 0, 12, 30, 18 + * -- nUNL overlap: |nUNL - UNL| = 5, with nUNL size: 18 + * -- with command line minimumQuorum = 50%, + * seen_reliable affected by nUNL + */ + + { + hash_set activeValidators; + //== Combinations == + std::array unlSizes = {34, 35, 39, 60}; + std::array nUnlPercent = {0, 20, 30, 50}; + for (auto us : unlSizes) + { + for (auto np : nUnlPercent) + { + auto validators = createValidatorList(us); + BEAST_EXPECT(validators); + if (validators) + { + std::uint32_t nUnlSize = us * np / 100; + auto unl = validators->getTrustedMasterKeys(); + hash_set nUnl; + auto it = unl.begin(); + for (std::uint32_t i = 0; i < nUnlSize; ++i) + { + nUnl.insert(*it); + ++it; + } + validators->setNegativeUnl(nUnl); + validators->updateTrusted(activeValidators); + BEAST_EXPECT( + validators->quorum() == + static_cast(std::ceil( + std::max((us - nUnlSize) * 0.8f, us * 0.6f)))); + } + } + } + } + + { + //== with UNL size 60 + auto validators = createValidatorList(60); + BEAST_EXPECT(validators); + if (validators) + { + hash_set activeValidators; + auto unl = validators->getTrustedMasterKeys(); + BEAST_EXPECT(unl.size() == 60); + { + //-- set == get, + //-- check quorum, with nUNL size: 0, 30, 18, 12 + auto nUnlChange = [&](std::uint32_t nUnlSize, + std::uint32_t quorum) -> bool { + hash_set nUnl; + auto it = unl.begin(); + for (std::uint32_t i = 0; i < nUnlSize; ++i) + { + nUnl.insert(*it); + ++it; + } + validators->setNegativeUnl(nUnl); + auto nUnl_temp = validators->getNegativeUnl(); + if (nUnl_temp.size() == nUnl.size()) + { + for (auto& n : nUnl_temp) + { + if (nUnl.find(n) == nUnl.end()) + return false; + } + validators->updateTrusted(activeValidators); + return validators->quorum() == quorum; + } + return false; + }; + BEAST_EXPECT(nUnlChange(0, 48)); + BEAST_EXPECT(nUnlChange(30, 36)); + BEAST_EXPECT(nUnlChange(18, 36)); + BEAST_EXPECT(nUnlChange(12, 39)); + } + + { + // nUNL overlap: |nUNL - UNL| = 5, with nUNL size: 18 + auto nUnl = validators->getNegativeUnl(); + BEAST_EXPECT(nUnl.size() == 12); + std::size_t ss = 33; + std::vector data(ss, 0); + data[0] = 0xED; + for (int i = 0; i < 6; ++i) + { + Slice s(data.data(), ss); + data[1]++; + nUnl.emplace(s); + } + validators->setNegativeUnl(nUnl); + validators->updateTrusted(activeValidators); + BEAST_EXPECT(validators->quorum() == 39); + } + } + } + + { + //== with UNL size 60 + //-- with command line minimumQuorum = 50%, + // seen_reliable affected by nUNL + auto validators = createValidatorList(60, 30); + BEAST_EXPECT(validators); + if (validators) + { + hash_set activeValidators; + hash_set unl = validators->getTrustedMasterKeys(); + auto it = unl.begin(); + for (std::uint32_t i = 0; i < 50; ++i) + { + activeValidators.insert(calcNodeID(*it)); + ++it; + } + validators->updateTrusted(activeValidators); + BEAST_EXPECT(validators->quorum() == 48); + hash_set nUnl; + it = unl.begin(); + for (std::uint32_t i = 0; i < 20; ++i) + { + nUnl.insert(*it); + ++it; + } + validators->setNegativeUnl(nUnl); + validators->updateTrusted(activeValidators); + BEAST_EXPECT(validators->quorum() == 30); + } + } + } + public: void run() override @@ -1276,6 +1455,7 @@ public: testApplyList(); testUpdateTrusted(); testExpires(); + testNegativeUNL(); } }; diff --git a/src/test/consensus/NegativeUNL_test.cpp b/src/test/consensus/NegativeUNL_test.cpp new file mode 100644 index 0000000000..547cd17aa7 --- /dev/null +++ b/src/test/consensus/NegativeUNL_test.cpp @@ -0,0 +1,2108 @@ +//----------------------------------------------------------------------------- +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +/* + * This file implements the following negative UNL related tests: + * -- test filling and applying ttUNL_MODIFY Tx and ledger update + * -- test ttUNL_MODIFY Tx failure without featureNegativeUNL amendment + * -- test the NegativeUNLVote class. The test cases are split to multiple + * test classes to allow parallel execution. + * -- test the negativeUNLFilter function + * + * Other negative UNL related tests such as ValidatorList and RPC related ones + * are put in their existing unit test files. + */ + +/** + * Test the size of the negative UNL in a ledger, + * also test if the ledger has ToDisalbe and/or ToReEnable + * + * @param l the ledger + * @param size the expected negative UNL size + * @param hasToDisable if expect ToDisable in ledger + * @param hasToReEnable if expect ToDisable in ledger + * @return true if meet all three expectation + */ +bool +negUnlSizeTest( + std::shared_ptr const& l, + size_t size, + bool hasToDisable, + bool hasToReEnable); + +/** + * Try to apply a ttUNL_MODIFY Tx, and test the apply result + * + * @param env the test environment + * @param view the OpenView of the ledger + * @param tx the ttUNL_MODIFY Tx + * @param pass if the Tx should be applied successfully + * @return true if meet the expectation of apply result + */ +bool +applyAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx, bool pass); + +/** + * Verify the content of negative UNL entries (public key and ledger sequence) + * of a ledger + * + * @param l the ledger + * @param nUnlLedgerSeq the expected PublicKeys and ledger Sequences + * @note nUnlLedgerSeq is copied so that it can be modified. + * @return true if meet the expectation + */ +bool +VerifyPubKeyAndSeq( + std::shared_ptr const& l, + hash_map nUnlLedgerSeq); + +/** + * Count the number of Tx in a TxSet + * + * @param txSet the TxSet + * @return the number of Tx + */ +std::size_t +countTx(std::shared_ptr const& txSet); + +/** + * Create fake public keys + * + * @param n the number of public keys + * @return a vector of public keys created + */ +std::vector +createPublicKeys(std::size_t n); + +/** + * Create ttUNL_MODIFY Tx + * + * @param disabling disabling or re-enabling a validator + * @param seq current ledger seq + * @param txKey the public key of the validator + * @return the ttUNL_MODIFY Tx + */ +STTx +createTx(bool disabling, LedgerIndex seq, PublicKey const& txKey); + +class NegativeUNL_test : public beast::unit_test::suite +{ + /** + * Test filling and applying ttUNL_MODIFY Tx, as well as ledger update: + * + * We will build a long history of ledgers, and try to apply different + * ttUNL_MODIFY Txes. We will check if the apply results meet expectations + * and if the ledgers are updated correctly. + */ + void + testNegativeUNL() + { + /* + * test cases: + * + * (1) the ledger after genesis + * -- cannot apply Disable Tx + * -- cannot apply ReEnable Tx + * -- nUNL empty + * -- no ToDisable + * -- no ToReEnable + * + * (2) a flag ledger + * -- apply an Disable Tx + * -- cannot apply the second Disable Tx + * -- cannot apply a ReEnable Tx + * -- nUNL empty + * -- has ToDisable with right nodeId + * -- no ToReEnable + * ++ extra test: first Disable Tx in ledger TxSet + * + * (3) ledgers before the next flag ledger + * -- nUNL empty + * -- has ToDisable with right nodeId + * -- no ToReEnable + * + * (4) next flag ledger + * -- nUNL size == 1, with right nodeId + * -- no ToDisable + * -- no ToReEnable + * -- cannot apply an Disable Tx with nodeId already in nUNL + * -- apply an Disable Tx with different nodeId + * -- cannot apply a ReEnable Tx with the same NodeId as Add + * -- cannot apply a ReEnable Tx with a NodeId not in nUNL + * -- apply a ReEnable Tx with a nodeId already in nUNL + * -- has ToDisable with right nodeId + * -- has ToReEnable with right nodeId + * -- nUNL size still 1, right nodeId + * + * (5) ledgers before the next flag ledger + * -- nUNL size == 1, right nodeId + * -- has ToDisable with right nodeId + * -- has ToReEnable with right nodeId + * + * (6) next flag ledger + * -- nUNL size == 1, different nodeId + * -- no ToDisable + * -- no ToReEnable + * -- apply an Disable Tx with different nodeId + * -- nUNL size still 1, right nodeId + * -- has ToDisable with right nodeId + * -- no ToReEnable + * + * (7) ledgers before the next flag ledger + * -- nUNL size still 1, right nodeId + * -- has ToDisable with right nodeId + * -- no ToReEnable + * + * (8) next flag ledger + * -- nUNL size == 2 + * -- apply a ReEnable Tx + * -- cannot apply second ReEnable Tx, even with right nodeId + * -- cannot apply an Disable Tx with the same NodeId as Remove + * -- nUNL size == 2 + * -- no ToDisable + * -- has ToReEnable with right nodeId + * + * (9) ledgers before the next flag ledger + * -- nUNL size == 2 + * -- no ToDisable + * -- has ToReEnable with right nodeId + * + * (10) next flag ledger + * -- nUNL size == 1 + * -- apply a ReEnable Tx + * -- nUNL size == 1 + * -- no ToDisable + * -- has ToReEnable with right nodeId + * + * (11) ledgers before the next flag ledger + * -- nUNL size == 1 + * -- no ToDisable + * -- has ToReEnable with right nodeId + * + * (12) next flag ledger + * -- nUNL size == 0 + * -- no ToDisable + * -- no ToReEnable + * + * (13) ledgers before the next flag ledger + * -- nUNL size == 0 + * -- no ToDisable + * -- no ToReEnable + * + * (14) next flag ledger + * -- nUNL size == 0 + * -- no ToDisable + * -- no ToReEnable + */ + + testcase("Create UNLModify Tx and apply to ledgers"); + + jtx::Env env(*this, jtx::supported_amendments() | featureNegativeUNL); + std::vector publicKeys = createPublicKeys(3); + // genesis ledger + auto l = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + BEAST_EXPECT(l->rules().enabled(featureNegativeUNL)); + + // Record the public keys and ledger sequences of expected negative UNL + // validators when we build the ledger history + hash_map nUnlLedgerSeq; + + { + //(1) the ledger after genesis, not a flag ledger + l = std::make_shared( + *l, env.app().timeKeeper().closeTime()); + + auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); + auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]); + + OpenView accum(&*l); + BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false)); + BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, false)); + accum.apply(*l); + BEAST_EXPECT(negUnlSizeTest(l, 0, false, false)); + } + + { + //(2) a flag ledger + // generate more ledgers + for (auto i = 0; i < 256 - 2; ++i) + { + l = std::make_shared( + *l, env.app().timeKeeper().closeTime()); + } + BEAST_EXPECT(l->isFlagLedger()); + l->updateNegativeUNL(); + + auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); + auto txDisable_1 = createTx(true, l->seq(), publicKeys[1]); + auto txReEnable_2 = createTx(false, l->seq(), publicKeys[2]); + + // can apply 1 and only 1 ToDisable Tx, + // cannot apply ToReEnable Tx, since negative UNL is empty + OpenView accum(&*l); + BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, true)); + BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_1, false)); + BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_2, false)); + accum.apply(*l); + auto good_size = negUnlSizeTest(l, 0, true, false); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnlToDisable() == publicKeys[0]); + //++ first ToDisable Tx in ledger's TxSet + uint256 txID = txDisable_0.getTransactionID(); + BEAST_EXPECT(l->txExists(txID)); + } + } + + { + //(3) ledgers before the next flag ledger + for (auto i = 0; i < 256; ++i) + { + auto good_size = negUnlSizeTest(l, 0, true, false); + BEAST_EXPECT(good_size); + if (good_size) + BEAST_EXPECT(l->negativeUnlToDisable() == publicKeys[0]); + l = std::make_shared( + *l, env.app().timeKeeper().closeTime()); + } + BEAST_EXPECT(l->isFlagLedger()); + l->updateNegativeUNL(); + + //(4) next flag ledger + // test if the ledger updated correctly + auto good_size = negUnlSizeTest(l, 1, false, false); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(*(l->negativeUnl().begin()) == publicKeys[0]); + nUnlLedgerSeq.emplace(publicKeys[0], l->seq()); + } + + auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); + auto txDisable_1 = createTx(true, l->seq(), publicKeys[1]); + auto txReEnable_0 = createTx(false, l->seq(), publicKeys[0]); + auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]); + auto txReEnable_2 = createTx(false, l->seq(), publicKeys[2]); + + OpenView accum(&*l); + BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false)); + BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_1, true)); + BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, false)); + BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_2, false)); + BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_0, true)); + accum.apply(*l); + good_size = negUnlSizeTest(l, 1, true, true); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[0])); + BEAST_EXPECT(l->negativeUnlToDisable() == publicKeys[1]); + BEAST_EXPECT(l->negativeUnlToReEnable() == publicKeys[0]); + // test sfFirstLedgerSequence + BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); + } + } + + { + //(5) ledgers before the next flag ledger + for (auto i = 0; i < 256; ++i) + { + auto good_size = negUnlSizeTest(l, 1, true, true); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[0])); + BEAST_EXPECT(l->negativeUnlToDisable() == publicKeys[1]); + BEAST_EXPECT(l->negativeUnlToReEnable() == publicKeys[0]); + } + l = std::make_shared( + *l, env.app().timeKeeper().closeTime()); + } + BEAST_EXPECT(l->isFlagLedger()); + l->updateNegativeUNL(); + + //(6) next flag ledger + // test if the ledger updated correctly + auto good_size = negUnlSizeTest(l, 1, false, false); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[1])); + } + + auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); + + OpenView accum(&*l); + BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, true)); + accum.apply(*l); + good_size = negUnlSizeTest(l, 1, true, false); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[1])); + BEAST_EXPECT(l->negativeUnlToDisable() == publicKeys[0]); + nUnlLedgerSeq.emplace(publicKeys[1], l->seq()); + nUnlLedgerSeq.erase(publicKeys[0]); + BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); + } + } + + { + //(7) ledgers before the next flag ledger + for (auto i = 0; i < 256; ++i) + { + auto good_size = negUnlSizeTest(l, 1, true, false); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[1])); + BEAST_EXPECT(l->negativeUnlToDisable() == publicKeys[0]); + } + l = std::make_shared( + *l, env.app().timeKeeper().closeTime()); + } + BEAST_EXPECT(l->isFlagLedger()); + l->updateNegativeUNL(); + + //(8) next flag ledger + // test if the ledger updated correctly + auto good_size = negUnlSizeTest(l, 2, false, false); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[0])); + BEAST_EXPECT(l->negativeUnl().count(publicKeys[1])); + nUnlLedgerSeq.emplace(publicKeys[0], l->seq()); + BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); + } + + auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); + auto txReEnable_0 = createTx(false, l->seq(), publicKeys[0]); + auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]); + + OpenView accum(&*l); + BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_0, true)); + BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, false)); + BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false)); + accum.apply(*l); + good_size = negUnlSizeTest(l, 2, false, true); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[0])); + BEAST_EXPECT(l->negativeUnl().count(publicKeys[1])); + BEAST_EXPECT(l->negativeUnlToReEnable() == publicKeys[0]); + BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); + } + } + + { + //(9) ledgers before the next flag ledger + for (auto i = 0; i < 256; ++i) + { + auto good_size = negUnlSizeTest(l, 2, false, true); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[0])); + BEAST_EXPECT(l->negativeUnl().count(publicKeys[1])); + BEAST_EXPECT(l->negativeUnlToReEnable() == publicKeys[0]); + } + l = std::make_shared( + *l, env.app().timeKeeper().closeTime()); + } + BEAST_EXPECT(l->isFlagLedger()); + l->updateNegativeUNL(); + + //(10) next flag ledger + // test if the ledger updated correctly + auto good_size = negUnlSizeTest(l, 1, false, false); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[1])); + nUnlLedgerSeq.erase(publicKeys[0]); + BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); + } + + auto txReEnable_1 = createTx(false, l->seq(), publicKeys[1]); + + OpenView accum(&*l); + BEAST_EXPECT(applyAndTestResult(env, accum, txReEnable_1, true)); + accum.apply(*l); + good_size = negUnlSizeTest(l, 1, false, true); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[1])); + BEAST_EXPECT(l->negativeUnlToReEnable() == publicKeys[1]); + BEAST_EXPECT(VerifyPubKeyAndSeq(l, nUnlLedgerSeq)); + } + } + + { + //(11) ledgers before the next flag ledger + for (auto i = 0; i < 256; ++i) + { + auto good_size = negUnlSizeTest(l, 1, false, true); + BEAST_EXPECT(good_size); + if (good_size) + { + BEAST_EXPECT(l->negativeUnl().count(publicKeys[1])); + BEAST_EXPECT(l->negativeUnlToReEnable() == publicKeys[1]); + } + l = std::make_shared( + *l, env.app().timeKeeper().closeTime()); + } + BEAST_EXPECT(l->isFlagLedger()); + l->updateNegativeUNL(); + + //(12) next flag ledger + BEAST_EXPECT(negUnlSizeTest(l, 0, false, false)); + } + + { + //(13) ledgers before the next flag ledger + for (auto i = 0; i < 256; ++i) + { + BEAST_EXPECT(negUnlSizeTest(l, 0, false, false)); + l = std::make_shared( + *l, env.app().timeKeeper().closeTime()); + } + BEAST_EXPECT(l->isFlagLedger()); + l->updateNegativeUNL(); + + //(14) next flag ledger + BEAST_EXPECT(negUnlSizeTest(l, 0, false, false)); + } + } + + void + run() override + { + testNegativeUNL(); + } +}; + +class NegativeUNLNoAmendment_test : public beast::unit_test::suite +{ + void + testNegativeUNLNoAmendment() + { + testcase("No negative UNL amendment"); + + jtx::Env env(*this, jtx::supported_amendments() - featureNegativeUNL); + std::vector publicKeys = createPublicKeys(1); + // genesis ledger + auto l = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + BEAST_EXPECT(!l->rules().enabled(featureNegativeUNL)); + + // generate more ledgers + for (auto i = 0; i < 256 - 1; ++i) + { + l = std::make_shared( + *l, env.app().timeKeeper().closeTime()); + } + BEAST_EXPECT(l->seq() == 256); + auto txDisable_0 = createTx(true, l->seq(), publicKeys[0]); + OpenView accum(&*l); + BEAST_EXPECT(applyAndTestResult(env, accum, txDisable_0, false)); + accum.apply(*l); + BEAST_EXPECT(negUnlSizeTest(l, 0, false, false)); + } + + void + run() override + { + testNegativeUNLNoAmendment(); + } +}; + +/** + * Utility class for creating validators and ledger history + */ +struct NetworkHistory +{ + using LedgerHistory = std::vector>; + /** + * + * Only reasonable parameters can be honored, + * e.g cannot hasToReEnable when nUNLSize == 0 + */ + struct Parameter + { + std::uint32_t numNodes; // number of validators + std::uint32_t negUNLSize; // size of negative UNL in the last ledger + bool hasToDisable; // if has ToDisable in the last ledger + bool hasToReEnable; // if has ToReEnable in the last ledger + /** + * if not specified, the number of ledgers in the history is calculated + * from negUNLSize, hasToDisable, and hasToReEnable + */ + std::optional numLedgers; + }; + + NetworkHistory(beast::unit_test::suite& suite, Parameter const& p) + : env(suite, jtx::supported_amendments() | featureNegativeUNL) + , param(p) + , validations(env.app().getValidations()) + { + createNodes(); + if (!param.numLedgers) + param.numLedgers = 256 * (param.negUNLSize + 1); + goodHistory = createLedgerHistory(); + } + + void + createNodes() + { + assert(param.numNodes <= 256); + UNLKeys = createPublicKeys(param.numNodes); + for (int i = 0; i < param.numNodes; ++i) + { + UNLKeySet.insert(UNLKeys[i]); + UNLNodeIDs.push_back(calcNodeID(UNLKeys[i])); + UNLNodeIDSet.insert(UNLNodeIDs.back()); + } + } + + /** + * create ledger history and apply needed ttUNL_MODIFY tx at flag ledgers + * @return + */ + bool + createLedgerHistory() + { + static uint256 fake_amemdment; // So we have different genesis ledgers + auto l = std::make_shared( + create_genesis, + env.app().config(), + std::vector{fake_amemdment++}, + env.app().getNodeFamily()); + history.push_back(l); + + // When putting validators into the negative UNL, we start with + // validator 0, then validator 1 ... + int nidx = 0; + while (l->seq() <= param.numLedgers) + { + l = std::make_shared( + *l, env.app().timeKeeper().closeTime()); + history.push_back(l); + + if (l->isFlagLedger()) + { + l->updateNegativeUNL(); + OpenView accum(&*l); + if (l->negativeUnl().size() < param.negUNLSize) + { + auto tx = createTx(true, l->seq(), UNLKeys[nidx]); + if (!applyAndTestResult(env, accum, tx, true)) + break; + ++nidx; + } + else if (l->negativeUnl().size() == param.negUNLSize) + { + if (param.hasToDisable) + { + auto tx = createTx(true, l->seq(), UNLKeys[nidx]); + if (!applyAndTestResult(env, accum, tx, true)) + break; + ++nidx; + } + if (param.hasToReEnable) + { + auto tx = createTx(false, l->seq(), UNLKeys[0]); + if (!applyAndTestResult(env, accum, tx, true)) + break; + } + } + accum.apply(*l); + } + l->updateSkipList(); + } + return negUnlSizeTest( + l, param.negUNLSize, param.hasToDisable, param.hasToReEnable); + } + + /** + * Create a validation + * @param ledger the ledger the validation validates + * @param v the validator + * @return the validation + */ + std::shared_ptr + createSTVal(std::shared_ptr const& ledger, NodeID const& v) + { + static auto keyPair = randomKeyPair(KeyType::secp256k1); + return std::make_shared( + env.app().timeKeeper().now(), + keyPair.first, + keyPair.second, + v, + [&](STValidation& v) { + v.setFieldH256(sfLedgerHash, ledger->info().hash); + v.setFieldU32(sfLedgerSequence, ledger->seq()); + v.setFlag(vfFullValidation); + }); + }; + + /** + * Walk the ledger history and create validation messages for the ledgers + * + * @tparam NeedValidation a function to decided if a validation is needed + * @param needVal if a validation is needed for this particular combination + * of ledger and validator + */ + template + void + walkHistoryAndAddValidations(NeedValidation&& needVal) + { + std::uint32_t curr = 0; + std::size_t need = 256 + 1; + // only last 256 + 1 ledgers need validations + if (history.size() > need) + curr = history.size() - need; + for (; curr != history.size(); ++curr) + { + for (std::size_t i = 0; i < param.numNodes; ++i) + { + if (needVal(history[curr], i)) + { + RCLValidation v(createSTVal(history[curr], UNLNodeIDs[i])); + v.setTrusted(); + validations.add(UNLNodeIDs[i], v); + } + } + } + } + + std::shared_ptr + lastLedger() const + { + return history.back(); + } + + jtx::Env env; + Parameter param; + RCLValidations& validations; + std::vector UNLKeys; + hash_set UNLKeySet; + std::vector UNLNodeIDs; + hash_set UNLNodeIDSet; + LedgerHistory history; + bool goodHistory; +}; + +auto defaultPreVote = [](NegativeUNLVote& vote) {}; +/** + * Create a NegativeUNLVote object. It then creates ttUNL_MODIFY Tx as its vote + * on negative UNL changes. + * + * @tparam PreVote a function to be called before vote + * @param history the ledger history + * @param myId the voting validator + * @param expect the number of ttUNL_MODIFY Tx expected + * @param pre the PreVote function + * @return true if the number of ttUNL_MODIFY Txes created meet expectation + */ +template +bool +voteAndCheck( + NetworkHistory& history, + NodeID const& myId, + std::size_t expect, + PreVote const& pre = defaultPreVote) +{ + NegativeUNLVote vote(myId, history.env.journal); + pre(vote); + auto txSet = std::make_shared( + SHAMapType::TRANSACTION, history.env.app().getNodeFamily()); + vote.doVoting( + history.lastLedger(), history.UNLKeySet, history.validations, txSet); + return countTx(txSet) == expect; +} + +/** + * Test the private member functions of NegativeUNLVote + */ +class NegativeUNLVoteInternal_test : public beast::unit_test::suite +{ + void + testAddTx() + { + testcase("Create UNLModify Tx"); + jtx::Env env(*this); + + NodeID myId(0xA0); + NegativeUNLVote vote(myId, env.journal); + + // one add, one remove + auto txSet = std::make_shared( + SHAMapType::TRANSACTION, env.app().getNodeFamily()); + PublicKey toDisableKey; + PublicKey toReEnableKey; + LedgerIndex seq(1234); + BEAST_EXPECT(countTx(txSet) == 0); + vote.addTx(seq, toDisableKey, NegativeUNLVote::ToDisable, txSet); + BEAST_EXPECT(countTx(txSet) == 1); + vote.addTx(seq, toReEnableKey, NegativeUNLVote::ToReEnable, txSet); + BEAST_EXPECT(countTx(txSet) == 2); + // content of a tx is implicitly tested after applied to a ledger + // in later test cases + } + + void + testPickOneCandidate() + { + testcase("Pick One Candidate"); + jtx::Env env(*this); + + NodeID myId(0xA0); + NegativeUNLVote vote(myId, env.journal); + + uint256 pad_0(0); + uint256 pad_f = ~pad_0; + NodeID n_1(1); + NodeID n_2(2); + NodeID n_3(3); + std::vector candidates({n_1}); + BEAST_EXPECT(vote.choose(pad_0, candidates) == n_1); + BEAST_EXPECT(vote.choose(pad_f, candidates) == n_1); + candidates.emplace_back(2); + BEAST_EXPECT(vote.choose(pad_0, candidates) == n_1); + BEAST_EXPECT(vote.choose(pad_f, candidates) == n_2); + candidates.emplace_back(3); + BEAST_EXPECT(vote.choose(pad_0, candidates) == n_1); + BEAST_EXPECT(vote.choose(pad_f, candidates) == n_3); + } + + void + testBuildScoreTableSpecialCases() + { + testcase("Build Score Table"); + /* + * 1. no skip list + * 2. short skip list + * 3. local node not enough history + * 4. a node double validated some seq + * 5. local node had enough validations but on a wrong chain + * 6. a good case, long enough history and perfect scores + */ + { + // 1. no skip list + NetworkHistory history = {*this, {10, 0, false, false, 1}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + NegativeUNLVote vote( + history.UNLNodeIDs[3], history.env.journal); + BEAST_EXPECT(!vote.buildScoreTable( + history.lastLedger(), + history.UNLNodeIDSet, + history.validations)); + } + } + + { + // 2. short skip list + NetworkHistory history = {*this, {10, 0, false, false, 256 / 2}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + NegativeUNLVote vote( + history.UNLNodeIDs[3], history.env.journal); + BEAST_EXPECT(!vote.buildScoreTable( + history.lastLedger(), + history.UNLNodeIDSet, + history.validations)); + } + } + + { + // 3. local node not enough history + NetworkHistory history = {*this, {10, 0, false, false, 256 + 2}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + NodeID myId = history.UNLNodeIDs[3]; + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { + // skip half my validations. + return !( + history.UNLNodeIDs[idx] == myId && + l->seq() % 2 == 0); + }); + NegativeUNLVote vote(myId, history.env.journal); + BEAST_EXPECT(!vote.buildScoreTable( + history.lastLedger(), + history.UNLNodeIDSet, + history.validations)); + } + } + + { + // 4. a node double validated some seq + // 5. local node had enough validations but on a wrong chain + NetworkHistory history = {*this, {10, 0, false, false, 256 + 2}}; + // We need two chains for these tests + bool wrongChainSuccess = history.goodHistory; + BEAST_EXPECT(wrongChainSuccess); + NetworkHistory::LedgerHistory wrongChain = + std::move(history.history); + // Create a new chain and use it as the one that majority of nodes + // follow + history.createLedgerHistory(); + BEAST_EXPECT(history.goodHistory); + + if (history.goodHistory && wrongChainSuccess) + { + NodeID myId = history.UNLNodeIDs[3]; + NodeID badNode = history.UNLNodeIDs[4]; + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { + // everyone but me + return !(history.UNLNodeIDs[idx] == myId); + }); + + // local node validate wrong chain + // a node double validates + for (auto& l : wrongChain) + { + RCLValidation v1(history.createSTVal(l, myId)); + history.validations.add(myId, v1); + RCLValidation v2(history.createSTVal(l, badNode)); + history.validations.add(badNode, v2); + } + + NegativeUNLVote vote(myId, history.env.journal); + + // local node still on wrong chain, can build a scoreTable, + // but all other nodes' scores are zero + auto scoreTable = vote.buildScoreTable( + wrongChain.back(), + history.UNLNodeIDSet, + history.validations); + BEAST_EXPECT(scoreTable); + if (scoreTable) + { + for (auto const& [n, score] : *scoreTable) + { + if (n == myId) + BEAST_EXPECT(score == 256); + else + BEAST_EXPECT(score == 0); + } + } + + // if local node switched to right history, but cannot build + // scoreTable because not enough local validations + BEAST_EXPECT(!vote.buildScoreTable( + history.lastLedger(), + history.UNLNodeIDSet, + history.validations)); + } + } + + { + // 6. a good case + NetworkHistory history = {*this, {10, 0, false, false, 256 + 1}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { return true; }); + NegativeUNLVote vote( + history.UNLNodeIDs[3], history.env.journal); + auto scoreTable = vote.buildScoreTable( + history.lastLedger(), + history.UNLNodeIDSet, + history.validations); + BEAST_EXPECT(scoreTable); + if (scoreTable) + { + for (auto const& [_, score] : *scoreTable) + { + (void)_; + BEAST_EXPECT(score == 256); + } + } + } + } + } + + /** + * Find all candidates and check if the number of candidates meets + * expectation + * + * @param vote the NegativeUNLVote object + * @param unl the validators + * @param negUnl the negative UNL validators + * @param scoreTable the score table of validators + * @param numDisable number of Disable candidates expected + * @param numReEnable number of ReEnable candidates expected + * @return true if the number of candidates meets expectation + */ + bool + checkCandidateSizes( + NegativeUNLVote& vote, + hash_set const& unl, + hash_set const& negUnl, + hash_map const& scoreTable, + std::size_t numDisable, + std::size_t numReEnable) + { + auto [disableCandidates, reEnableCandidates] = + vote.findAllCandidates(unl, negUnl, scoreTable); + bool rightDisable = disableCandidates.size() == numDisable; + bool rightReEnable = reEnableCandidates.size() == numReEnable; + return rightDisable && rightReEnable; + }; + + void + testFindAllCandidates() + { + testcase("Find All Candidates"); + /* + * -- unl size: 35 + * -- negUnl size: 3 + * + * 0. all good scores + * 1. all bad scores + * 2. all between watermarks + * 3. 2 good scorers in negUnl + * 4. 2 bad scorers not in negUnl + * 5. 2 in negUnl but not in unl, have a remove candidate from score + * table + * 6. 2 in negUnl but not in unl, no remove candidate from score table + * 7. 2 new validators have good scores, already in negUnl + * 8. 2 new validators have bad scores, not in negUnl + * 9. expired the new validators have bad scores, not in negUnl + */ + NetworkHistory history = {*this, {35, 0, false, false, 0}}; + + hash_set negUnl_012; + for (std::uint32_t i = 0; i < 3; ++i) + negUnl_012.insert(history.UNLNodeIDs[i]); + + // build a good scoreTable to use, or copy and modify + hash_map goodScoreTable; + for (auto const& n : history.UNLNodeIDs) + goodScoreTable[n] = NegativeUNLVote::negativeUnlHighWaterMark + 1; + + NegativeUNLVote vote(history.UNLNodeIDs[0], history.env.journal); + + { + // all good scores + BEAST_EXPECT(checkCandidateSizes( + vote, history.UNLNodeIDSet, negUnl_012, goodScoreTable, 0, 3)); + } + { + // all bad scores + hash_map scoreTable; + for (auto& n : history.UNLNodeIDs) + scoreTable[n] = NegativeUNLVote::negativeUnlLowWaterMark - 1; + BEAST_EXPECT(checkCandidateSizes( + vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 35 - 3, 0)); + } + { + // all between watermarks + hash_map scoreTable; + for (auto& n : history.UNLNodeIDs) + scoreTable[n] = NegativeUNLVote::negativeUnlLowWaterMark + 1; + BEAST_EXPECT(checkCandidateSizes( + vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 0, 0)); + } + + { + // 2 good scorers in negUnl + auto scoreTable = goodScoreTable; + scoreTable[*negUnl_012.begin()] = + NegativeUNLVote::negativeUnlLowWaterMark + 1; + BEAST_EXPECT(checkCandidateSizes( + vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 0, 2)); + } + + { + // 2 bad scorers not in negUnl + auto scoreTable = goodScoreTable; + scoreTable[history.UNLNodeIDs[11]] = + NegativeUNLVote::negativeUnlLowWaterMark - 1; + scoreTable[history.UNLNodeIDs[12]] = + NegativeUNLVote::negativeUnlLowWaterMark - 1; + BEAST_EXPECT(checkCandidateSizes( + vote, history.UNLNodeIDSet, negUnl_012, scoreTable, 2, 3)); + } + + { + // 2 in negUnl but not in unl, have a remove candidate from score + // table + hash_set UNL_temp = history.UNLNodeIDSet; + UNL_temp.erase(history.UNLNodeIDs[0]); + UNL_temp.erase(history.UNLNodeIDs[1]); + BEAST_EXPECT(checkCandidateSizes( + vote, UNL_temp, negUnl_012, goodScoreTable, 0, 3)); + } + + { + // 2 in negUnl but not in unl, no remove candidate from score table + auto scoreTable = goodScoreTable; + scoreTable.erase(history.UNLNodeIDs[0]); + scoreTable.erase(history.UNLNodeIDs[1]); + scoreTable[history.UNLNodeIDs[2]] = + NegativeUNLVote::negativeUnlLowWaterMark + 1; + hash_set UNL_temp = history.UNLNodeIDSet; + UNL_temp.erase(history.UNLNodeIDs[0]); + UNL_temp.erase(history.UNLNodeIDs[1]); + BEAST_EXPECT(checkCandidateSizes( + vote, UNL_temp, negUnl_012, scoreTable, 0, 2)); + } + + { + // 2 new validators + NodeID new_1(0xbead); + NodeID new_2(0xbeef); + hash_set nowTrusted = {new_1, new_2}; + hash_set UNL_temp = history.UNLNodeIDSet; + UNL_temp.insert(new_1); + UNL_temp.insert(new_2); + vote.newValidators(256, nowTrusted); + { + // 2 new validators have good scores, already in negUnl + auto scoreTable = goodScoreTable; + scoreTable[new_1] = + NegativeUNLVote::negativeUnlHighWaterMark + 1; + scoreTable[new_2] = + NegativeUNLVote::negativeUnlHighWaterMark + 1; + hash_set negUnl_temp = negUnl_012; + negUnl_temp.insert(new_1); + negUnl_temp.insert(new_2); + BEAST_EXPECT(checkCandidateSizes( + vote, UNL_temp, negUnl_temp, scoreTable, 0, 3 + 2)); + } + { + // 2 new validators have bad scores, not in negUnl + auto scoreTable = goodScoreTable; + scoreTable[new_1] = 0; + scoreTable[new_2] = 0; + BEAST_EXPECT(checkCandidateSizes( + vote, UNL_temp, negUnl_012, scoreTable, 0, 3)); + } + { + // expired the new validators have bad scores, not in negUnl + vote.purgeNewValidators( + 256 + NegativeUNLVote::newValidatorDisableSkip + 1); + auto scoreTable = goodScoreTable; + scoreTable[new_1] = 0; + scoreTable[new_2] = 0; + BEAST_EXPECT(checkCandidateSizes( + vote, UNL_temp, negUnl_012, scoreTable, 2, 3)); + } + } + } + + void + testFindAllCandidatesCombination() + { + testcase("Find All Candidates Combination"); + /* + * == combination 1: + * -- unl size: 34, 35, 80 + * -- nUnl size: 0, 50%, all + * -- score pattern: all 0, all negativeUnlLowWaterMark & +1 & -1, all + * negativeUnlHighWaterMark & +1 & -1, all 100% + * + * == combination 2: + * -- unl size: 34, 35, 80 + * -- negativeUnl size: 0, all + * -- nUnl size: one on, one off, one on, one off, + * -- score pattern: 2*(negativeUnlLowWaterMark, +1, -1) & + * 2*(negativeUnlHighWaterMark, +1, -1) & rest + * negativeUnlMinLocalValsToVote + */ + + jtx::Env env(*this); + + NodeID myId(0xA0); + NegativeUNLVote vote(myId, env.journal); + + std::array unlSizes = {34, 35, 80}; + std::array nUnlPercent = {0, 50, 100}; + std::array scores = { + 0, + NegativeUNLVote::negativeUnlLowWaterMark - 1, + NegativeUNLVote::negativeUnlLowWaterMark, + NegativeUNLVote::negativeUnlLowWaterMark + 1, + NegativeUNLVote::negativeUnlHighWaterMark - 1, + NegativeUNLVote::negativeUnlHighWaterMark, + NegativeUNLVote::negativeUnlHighWaterMark + 1, + NegativeUNLVote::negativeUnlMinLocalValsToVote}; + + //== combination 1: + { + auto fillScoreTable = + [&](std::uint32_t unl_size, + std::uint32_t nUnl_size, + std::uint32_t score, + hash_set& unl, + hash_set& negUnl, + hash_map& scoreTable) { + std::vector nodeIDs; + std::vector keys = createPublicKeys(unl_size); + for (auto const& k : keys) + { + nodeIDs.emplace_back(calcNodeID(k)); + unl.emplace(nodeIDs.back()); + scoreTable[nodeIDs.back()] = score; + } + for (std::uint32_t i = 0; i < nUnl_size; ++i) + negUnl.insert(nodeIDs[i]); + }; + + for (auto us : unlSizes) + { + for (auto np : nUnlPercent) + { + for (auto score : scores) + { + hash_set unl; + hash_set negUnl; + hash_map scoreTable; + fillScoreTable( + us, us * np / 100, score, unl, negUnl, scoreTable); + BEAST_EXPECT(unl.size() == us); + BEAST_EXPECT(negUnl.size() == us * np / 100); + BEAST_EXPECT(scoreTable.size() == us); + + std::size_t toDisable_expect = 0; + std::size_t toReEnable_expect = 0; + if (np == 0) + { + if (score < + NegativeUNLVote::negativeUnlLowWaterMark) + { + toDisable_expect = us; + } + } + else if (np == 50) + { + if (score > + NegativeUNLVote::negativeUnlHighWaterMark) + { + toReEnable_expect = us * np / 100; + } + } + else + { + if (score > + NegativeUNLVote::negativeUnlHighWaterMark) + { + toReEnable_expect = us; + } + } + BEAST_EXPECT(checkCandidateSizes( + vote, + unl, + negUnl, + scoreTable, + toDisable_expect, + toReEnable_expect)); + } + } + } + + //== combination 2: + { + auto fillScoreTable = + [&](std::uint32_t unl_size, + std::uint32_t nUnl_percent, + hash_set& unl, + hash_set& negUnl, + hash_map& scoreTable) { + std::vector nodeIDs; + std::vector keys = + createPublicKeys(unl_size); + for (auto const& k : keys) + { + nodeIDs.emplace_back(calcNodeID(k)); + unl.emplace(nodeIDs.back()); + } + + std::uint32_t nIdx = 0; + for (auto score : scores) + { + scoreTable[nodeIDs[nIdx++]] = score; + scoreTable[nodeIDs[nIdx++]] = score; + } + for (; nIdx < unl_size;) + { + scoreTable[nodeIDs[nIdx++]] = scores.back(); + } + + if (nUnl_percent == 100) + { + negUnl = unl; + } + else if (nUnl_percent == 50) + { + for (std::uint32_t i = 1; i < unl_size; i += 2) + negUnl.insert(nodeIDs[i]); + } + }; + + for (auto us : unlSizes) + { + for (auto np : nUnlPercent) + { + hash_set unl; + hash_set negUnl; + hash_map scoreTable; + + fillScoreTable(us, np, unl, negUnl, scoreTable); + BEAST_EXPECT(unl.size() == us); + BEAST_EXPECT(negUnl.size() == us * np / 100); + BEAST_EXPECT(scoreTable.size() == us); + + std::size_t toDisable_expect = 0; + std::size_t toReEnable_expect = 0; + if (np == 0) + { + toDisable_expect = 4; + } + else if (np == 50) + { + toReEnable_expect = negUnl.size() - 6; + } + else + { + toReEnable_expect = negUnl.size() - 12; + } + BEAST_EXPECT(checkCandidateSizes( + vote, + unl, + negUnl, + scoreTable, + toDisable_expect, + toReEnable_expect)); + } + } + } + } + } + + void + testNewValidators() + { + testcase("New Validators"); + jtx::Env env(*this); + + NodeID myId(0xA0); + NegativeUNLVote vote(myId, env.journal); + + // test cases: + // newValidators_ of the NegativeUNLVote empty, add one + // add a new one and one already added + // add a new one and some already added + // purge and see some are expired + + NodeID n1(0xA1); + NodeID n2(0xA2); + NodeID n3(0xA3); + + vote.newValidators(2, {n1}); + BEAST_EXPECT(vote.newValidators_.size() == 1); + if (vote.newValidators_.size() == 1) + { + BEAST_EXPECT(vote.newValidators_.begin()->first == n1); + BEAST_EXPECT(vote.newValidators_.begin()->second == 2); + } + + vote.newValidators(3, {n1, n2}); + BEAST_EXPECT(vote.newValidators_.size() == 2); + if (vote.newValidators_.size() == 2) + { + BEAST_EXPECT(vote.newValidators_[n1] == 2); + BEAST_EXPECT(vote.newValidators_[n2] == 3); + } + + vote.newValidators( + NegativeUNLVote::newValidatorDisableSkip, {n1, n2, n3}); + BEAST_EXPECT(vote.newValidators_.size() == 3); + if (vote.newValidators_.size() == 3) + { + BEAST_EXPECT(vote.newValidators_[n1] == 2); + BEAST_EXPECT(vote.newValidators_[n2] == 3); + BEAST_EXPECT( + vote.newValidators_[n3] == + NegativeUNLVote::newValidatorDisableSkip); + } + + vote.purgeNewValidators(NegativeUNLVote::newValidatorDisableSkip + 2); + BEAST_EXPECT(vote.newValidators_.size() == 3); + vote.purgeNewValidators(NegativeUNLVote::newValidatorDisableSkip + 3); + BEAST_EXPECT(vote.newValidators_.size() == 2); + vote.purgeNewValidators(NegativeUNLVote::newValidatorDisableSkip + 4); + BEAST_EXPECT(vote.newValidators_.size() == 1); + BEAST_EXPECT(vote.newValidators_.begin()->first == n3); + BEAST_EXPECT( + vote.newValidators_.begin()->second == + NegativeUNLVote::newValidatorDisableSkip); + } + + void + run() override + { + testAddTx(); + testPickOneCandidate(); + testBuildScoreTableSpecialCases(); + testFindAllCandidates(); + testFindAllCandidatesCombination(); + testNewValidators(); + } +}; + +/** + * Rest the build score table function of NegativeUNLVote. + * This was a part of NegativeUNLVoteInternal. It is redundant and has long + * runtime. So we separate it out as a manual test. + */ +class NegativeUNLVoteScoreTable_test : public beast::unit_test::suite +{ + void + testBuildScoreTableCombination() + { + testcase("Build Score Table Combination"); + /* + * local node good history, correct scores: + * == combination: + * -- unl size: 10, 34, 35, 50 + * -- score pattern: all 0, all 50%, all 100%, two 0% two 50% rest 100% + */ + std::array unlSizes = {10, 34, 35, 50}; + std::array, 4> scorePattern = { + {{{0, 0, 0}}, {{50, 50, 50}}, {{100, 100, 100}}, {{0, 50, 100}}}}; + + for (auto unlSize : unlSizes) + { + for (std::uint32_t sp = 0; sp < 4; ++sp) + { + NetworkHistory history = { + *this, {unlSize, 0, false, false, 256 + 2}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + NodeID myId = history.UNLNodeIDs[3]; + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { + std::size_t k; + if (idx < 2) + k = 0; + else if (idx < 4) + k = 1; + else + k = 2; + + bool add_50 = + scorePattern[sp][k] == 50 && l->seq() % 2 == 0; + bool add_100 = scorePattern[sp][k] == 100; + bool add_me = history.UNLNodeIDs[idx] == myId; + return add_50 || add_100 || add_me; + }); + + NegativeUNLVote vote(myId, history.env.journal); + auto scoreTable = vote.buildScoreTable( + history.lastLedger(), + history.UNLNodeIDSet, + history.validations); + BEAST_EXPECT(scoreTable); + if (scoreTable) + { + std::uint32_t i = 0; // looping unl + auto checkScores = [&](std::uint32_t score, + std::uint32_t k) -> bool { + if (history.UNLNodeIDs[i] == myId) + return score == 256; + if (scorePattern[sp][k] == 0) + return score == 0; + if (scorePattern[sp][k] == 50) + return score == 256 / 2; + if (scorePattern[sp][k] == 100) + return score == 256; + else + return false; + }; + for (; i < 2; ++i) + { + BEAST_EXPECT(checkScores( + (*scoreTable)[history.UNLNodeIDs[i]], 0)); + } + for (; i < 4; ++i) + { + BEAST_EXPECT(checkScores( + (*scoreTable)[history.UNLNodeIDs[i]], 1)); + } + for (; i < unlSize; ++i) + { + BEAST_EXPECT(checkScores( + (*scoreTable)[history.UNLNodeIDs[i]], 2)); + } + } + } + } + } + } + + void + run() override + { + testBuildScoreTableCombination(); + } +}; + +/* + * Test the doVoting function of NegativeUNLVote. + * The test cases are split to 5 classes for parallel execution. + * + * Voting tests: (use hasToDisable and hasToReEnable in some of the cases) + * + * == all good score, nUnl empty + * -- txSet.size = 0 + * == all good score, nUnl not empty (use hasToDisable) + * -- txSet.size = 1 + * + * == 2 nodes offline, nUnl empty (use hasToReEnable) + * -- txSet.size = 1 + * == 2 nodes offline, in nUnl + * -- txSet.size = 0 + * + * == 2 nodes offline, not in nUnl, but maxListed + * -- txSet.size = 0 + * + * == 2 nodes offline including me, not in nUnl + * -- txSet.size = 0 + * == 2 nodes offline, not in negativeUnl, but I'm not a validator + * -- txSet.size = 0 + * == 2 in nUnl, but not in unl, no other remove candidates + * -- txSet.size = 1 + * + * == 2 new validators have bad scores + * -- txSet.size = 0 + * == 2 expired new validators have bad scores + * -- txSet.size = 1 + */ + +class NegativeUNLVoteGoodScore_test : public beast::unit_test::suite +{ + void + testDoVoting() + { + testcase("Do Voting"); + + { + //== all good score, negativeUnl empty + //-- txSet.size = 0 + NetworkHistory history = {*this, {51, 0, false, false, {}}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { return true; }); + BEAST_EXPECT(voteAndCheck(history, history.UNLNodeIDs[0], 0)); + } + } + + { + // all good score, negativeUnl not empty (use hasToDisable) + //-- txSet.size = 1 + NetworkHistory history = {*this, {37, 0, true, false, {}}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { return true; }); + BEAST_EXPECT(voteAndCheck(history, history.UNLNodeIDs[0], 1)); + } + } + } + + void + run() override + { + testDoVoting(); + } +}; + +class NegativeUNLVoteOffline_test : public beast::unit_test::suite +{ + void + testDoVoting() + { + testcase("Do Voting"); + + { + //== 2 nodes offline, negativeUnl empty (use hasToReEnable) + //-- txSet.size = 1 + NetworkHistory history = {*this, {29, 1, false, true, {}}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { + // skip node 0 and node 1 + return idx > 1; + }); + BEAST_EXPECT( + voteAndCheck(history, history.UNLNodeIDs.back(), 1)); + } + } + + { + // 2 nodes offline, in negativeUnl + //-- txSet.size = 0 + NetworkHistory history = {*this, {30, 1, true, false, {}}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + NodeID n1 = + calcNodeID(*history.lastLedger()->negativeUnl().begin()); + NodeID n2 = + calcNodeID(*history.lastLedger()->negativeUnlToDisable()); + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { + // skip node 0 and node 1 + return history.UNLNodeIDs[idx] != n1 && + history.UNLNodeIDs[idx] != n2; + }); + BEAST_EXPECT( + voteAndCheck(history, history.UNLNodeIDs.back(), 0)); + } + } + } + + void + run() override + { + testDoVoting(); + } +}; + +class NegativeUNLVoteMaxListed_test : public beast::unit_test::suite +{ + void + testDoVoting() + { + testcase("Do Voting"); + + { + // 2 nodes offline, not in negativeUnl, but maxListed + //-- txSet.size = 0 + NetworkHistory history = {*this, {32, 8, true, true, {}}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { + // skip node 0 ~ 10 + return idx > 10; + }); + BEAST_EXPECT( + voteAndCheck(history, history.UNLNodeIDs.back(), 0)); + } + } + } + + void + run() override + { + testDoVoting(); + } +}; + +class NegativeUNLVoteRetiredValidator_test : public beast::unit_test::suite +{ + void + testDoVoting() + { + testcase("Do Voting"); + + { + //== 2 nodes offline including me, not in negativeUnl + //-- txSet.size = 0 + NetworkHistory history = {*this, {35, 0, false, false, {}}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { return idx > 1; }); + BEAST_EXPECT(voteAndCheck(history, history.UNLNodeIDs[0], 0)); + } + } + + { + // 2 nodes offline, not in negativeUnl, but I'm not a validator + //-- txSet.size = 0 + NetworkHistory history = {*this, {40, 0, false, false, {}}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { return idx > 1; }); + BEAST_EXPECT(voteAndCheck(history, NodeID(0xdeadbeef), 0)); + } + } + + { + //== 2 in negativeUnl, but not in unl, no other remove candidates + //-- txSet.size = 1 + NetworkHistory history = {*this, {25, 2, false, false, {}}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { return idx > 1; }); + BEAST_EXPECT(voteAndCheck( + history, + history.UNLNodeIDs.back(), + 1, + [&](NegativeUNLVote& vote) { + history.UNLKeySet.erase(history.UNLKeys[0]); + history.UNLKeySet.erase(history.UNLKeys[1]); + })); + } + } + } + + void + run() override + { + testDoVoting(); + } +}; + +class NegativeUNLVoteNewValidator_test : public beast::unit_test::suite +{ + void + testDoVoting() + { + testcase("Do Voting"); + + { + //== 2 new validators have bad scores + //-- txSet.size = 0 + NetworkHistory history = {*this, {15, 0, false, false, {}}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { return true; }); + BEAST_EXPECT(voteAndCheck( + history, + history.UNLNodeIDs[0], + 0, + [&](NegativeUNLVote& vote) { + auto extra_key_1 = + randomKeyPair(KeyType::ed25519).first; + auto extra_key_2 = + randomKeyPair(KeyType::ed25519).first; + history.UNLKeySet.insert(extra_key_1); + history.UNLKeySet.insert(extra_key_2); + hash_set nowTrusted; + nowTrusted.insert(calcNodeID(extra_key_1)); + nowTrusted.insert(calcNodeID(extra_key_2)); + vote.newValidators( + history.lastLedger()->seq(), nowTrusted); + })); + } + } + + { + //== 2 expired new validators have bad scores + //-- txSet.size = 1 + NetworkHistory history = { + *this, + {21, + 0, + false, + false, + NegativeUNLVote::newValidatorDisableSkip * 2}}; + BEAST_EXPECT(history.goodHistory); + if (history.goodHistory) + { + history.walkHistoryAndAddValidations( + [&](std::shared_ptr const& l, + std::size_t idx) -> bool { return true; }); + BEAST_EXPECT(voteAndCheck( + history, + history.UNLNodeIDs[0], + 1, + [&](NegativeUNLVote& vote) { + auto extra_key_1 = + randomKeyPair(KeyType::ed25519).first; + auto extra_key_2 = + randomKeyPair(KeyType::ed25519).first; + history.UNLKeySet.insert(extra_key_1); + history.UNLKeySet.insert(extra_key_2); + hash_set nowTrusted; + nowTrusted.insert(calcNodeID(extra_key_1)); + nowTrusted.insert(calcNodeID(extra_key_2)); + vote.newValidators(256, nowTrusted); + })); + } + } + } + + void + run() override + { + testDoVoting(); + } +}; + +class NegativeUNLVoteFilterValidations_test : public beast::unit_test::suite +{ + void + testFilterValidations() + { + testcase("Filter Validations"); + jtx::Env env(*this); + auto l = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + auto createSTVal = [&](std::pair const& keys) { + return std::make_shared( + env.app().timeKeeper().now(), + keys.first, + keys.second, + calcNodeID(keys.first), + [&](STValidation& v) { + v.setFieldH256(sfLedgerHash, l->info().hash); + v.setFieldU32(sfLedgerSequence, l->seq()); + v.setFlag(vfFullValidation); + }); + }; + + // create keys and validations + std::uint32_t numNodes = 10; + std::uint32_t negUnlSize = 3; + std::vector cfgKeys; + hash_set activeValidators; + hash_set nUnlKeys; + std::vector> vals; + for (int i = 0; i < numNodes; ++i) + { + auto keyPair = randomKeyPair(KeyType::secp256k1); + vals.emplace_back(createSTVal(keyPair)); + cfgKeys.push_back(toBase58(TokenType::NodePublic, keyPair.first)); + activeValidators.emplace(calcNodeID(keyPair.first)); + if (i < negUnlSize) + { + nUnlKeys.insert(keyPair.first); + } + } + + // setup the ValidatorList + auto& validators = env.app().validators(); + auto& local = *nUnlKeys.begin(); + std::vector cfgPublishers; + validators.load(local, cfgKeys, cfgPublishers); + validators.updateTrusted(activeValidators); + BEAST_EXPECT(validators.getTrustedMasterKeys().size() == numNodes); + validators.setNegativeUnl(nUnlKeys); + BEAST_EXPECT(validators.getNegativeUnl().size() == negUnlSize); + + // test the filter + BEAST_EXPECT(vals.size() == numNodes); + vals = validators.negativeUNLFilter(std::move(vals)); + BEAST_EXPECT(vals.size() == numNodes - negUnlSize); + } + + void + run() override + { + testFilterValidations(); + } +}; + +class NegativeUNLgRPC_test : public beast::unit_test::suite +{ + template + std::string + toByteString(T const& data) + { + const char* bytes = reinterpret_cast(data.data()); + return {bytes, data.size()}; + } + + void + testGRPC() + { + testcase("gRPC test"); + + auto gRpcTest = [this]( + std::uint32_t negUnlSize, + bool hasToDisable, + bool hasToReEnable) -> bool { + NetworkHistory history = { + *this, {20, negUnlSize, hasToDisable, hasToReEnable, {}}}; + if (!history.goodHistory) + return false; + + auto const& negUnlObject = + history.lastLedger()->read(keylet::negativeUNL()); + if (!negUnlSize && !hasToDisable && !hasToReEnable && !negUnlObject) + return true; + if (!negUnlObject) + return false; + + org::xrpl::rpc::v1::NegativeUnl to; + ripple::RPC::convert(to, *negUnlObject); + bool goodSize = to.negative_unl_entries_size() == negUnlSize && + to.has_validator_to_disable() == hasToDisable && + to.has_validator_to_re_enable() == hasToReEnable; + if (!goodSize) + return false; + + if (negUnlSize) + { + if (!negUnlObject->isFieldPresent(sfNegativeUNL)) + return false; + auto const& nUnlData = + negUnlObject->getFieldArray(sfNegativeUNL); + if (nUnlData.size() != negUnlSize) + return false; + int idx = 0; + for (auto const& n : nUnlData) + { + if (!n.isFieldPresent(sfPublicKey) || + !n.isFieldPresent(sfFirstLedgerSequence)) + return false; + + if (!to.negative_unl_entries(idx).has_ledger_sequence() || + !to.negative_unl_entries(idx).has_public_key()) + return false; + + if (to.negative_unl_entries(idx).public_key().value() != + toByteString(n.getFieldVL(sfPublicKey))) + return false; + + if (to.negative_unl_entries(idx) + .ledger_sequence() + .value() != n.getFieldU32(sfFirstLedgerSequence)) + return false; + + ++idx; + } + } + + if (hasToDisable) + { + if (!negUnlObject->isFieldPresent(sfNegativeUNLToDisable)) + return false; + if (to.validator_to_disable().value() != + toByteString( + negUnlObject->getFieldVL(sfNegativeUNLToDisable))) + return false; + } + + if (hasToReEnable) + { + if (!negUnlObject->isFieldPresent(sfNegativeUNLToReEnable)) + return false; + if (to.validator_to_re_enable().value() != + toByteString( + negUnlObject->getFieldVL(sfNegativeUNLToReEnable))) + return false; + } + + return true; + }; + + BEAST_EXPECT(gRpcTest(0, false, false)); + BEAST_EXPECT(gRpcTest(2, true, true)); + } + + void + run() override + { + testGRPC(); + } +}; + +BEAST_DEFINE_TESTSUITE(NegativeUNL, ledger, ripple); +BEAST_DEFINE_TESTSUITE(NegativeUNLNoAmendment, ledger, ripple); + +BEAST_DEFINE_TESTSUITE(NegativeUNLVoteInternal, consensus, ripple); +BEAST_DEFINE_TESTSUITE_MANUAL(NegativeUNLVoteScoreTable, consensus, ripple); +BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteGoodScore, consensus, ripple, 1); +BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteOffline, consensus, ripple, 1); +BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteMaxListed, consensus, ripple, 1); +BEAST_DEFINE_TESTSUITE_PRIO( + NegativeUNLVoteRetiredValidator, + consensus, + ripple, + 1); +BEAST_DEFINE_TESTSUITE_PRIO(NegativeUNLVoteNewValidator, consensus, ripple, 1); +BEAST_DEFINE_TESTSUITE(NegativeUNLVoteFilterValidations, consensus, ripple); +BEAST_DEFINE_TESTSUITE(NegativeUNLgRPC, ledger, ripple); + +/////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////// +bool +negUnlSizeTest( + std::shared_ptr const& l, + size_t size, + bool hasToDisable, + bool hasToReEnable) +{ + bool sameSize = l->negativeUnl().size() == size; + bool sameToDisable = + (l->negativeUnlToDisable() != boost::none) == hasToDisable; + bool sameToReEnable = + (l->negativeUnlToReEnable() != boost::none) == hasToReEnable; + + return sameSize && sameToDisable && sameToReEnable; +} + +bool +applyAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx, bool pass) +{ + auto res = apply(env.app(), view, tx, ApplyFlags::tapNONE, env.journal); + if (pass) + return res.first == tesSUCCESS; + else + return res.first == tefFAILURE || res.first == temDISABLED; +} + +bool +VerifyPubKeyAndSeq( + std::shared_ptr const& l, + hash_map nUnlLedgerSeq) +{ + auto sle = l->read(keylet::negativeUNL()); + if (!sle) + return false; + if (!sle->isFieldPresent(sfNegativeUNL)) + return false; + + auto const& nUnlData = sle->getFieldArray(sfNegativeUNL); + if (nUnlData.size() != nUnlLedgerSeq.size()) + return false; + + for (auto const& n : nUnlData) + { + if (!n.isFieldPresent(sfFirstLedgerSequence) || + !n.isFieldPresent(sfPublicKey)) + return false; + + auto seq = n.getFieldU32(sfFirstLedgerSequence); + auto d = n.getFieldVL(sfPublicKey); + auto s = makeSlice(d); + if (!publicKeyType(s)) + return false; + PublicKey pk(s); + auto it = nUnlLedgerSeq.find(pk); + if (it == nUnlLedgerSeq.end()) + return false; + if (it->second != seq) + return false; + nUnlLedgerSeq.erase(it); + } + return nUnlLedgerSeq.size() == 0; +} + +std::size_t +countTx(std::shared_ptr const& txSet) +{ + std::size_t count = 0; + for (auto i = txSet->begin(); i != txSet->end(); ++i) + { + ++count; + } + return count; +}; + +std::vector +createPublicKeys(std::size_t n) +{ + std::vector keys; + std::size_t ss = 33; + std::vector data(ss, 0); + data[0] = 0xED; + for (int i = 0; i < n; ++i) + { + data[1]++; + Slice s(data.data(), ss); + keys.emplace_back(s); + } + return keys; +} + +STTx +createTx(bool disabling, LedgerIndex seq, PublicKey const& txKey) +{ + auto fill = [&](auto& obj) { + obj.setFieldU8(sfUNLModifyDisabling, disabling ? 1 : 0); + obj.setFieldU32(sfLedgerSequence, seq); + obj.setFieldVL(sfUNLModifyValidator, txKey); + }; + return STTx(ttUNL_MODIFY, fill); +} + +} // namespace test +} // namespace ripple diff --git a/src/test/consensus/Validations_test.cpp b/src/test/consensus/Validations_test.cpp index 9455d3931b..2473c9a7f3 100644 --- a/src/test/consensus/Validations_test.cpp +++ b/src/test/consensus/Validations_test.cpp @@ -707,10 +707,18 @@ class Validations_test : public beast::unit_test::suite Node a = harness.makeNode(); Ledger ledgerA = h["a"]; - BEAST_EXPECT(ValStatus::current == harness.add(a.validate(ledgerA))); BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerA.id())); + + // Keep the validation from expire harness.clock().advance(harness.parms().validationSET_EXPIRES); + harness.vals().setSeqToKeep(ledgerA.seq()); + harness.vals().expire(); + BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerA.id())); + + // Allow the validation to expire + harness.clock().advance(harness.parms().validationSET_EXPIRES); + harness.vals().setSeqToKeep(++ledgerA.seq()); harness.vals().expire(); BEAST_EXPECT(!harness.vals().numTrustedForLedger(ledgerA.id())); } diff --git a/src/test/rpc/ValidatorRPC_test.cpp b/src/test/rpc/ValidatorRPC_test.cpp index 51050c679d..a43eba2932 100644 --- a/src/test/rpc/ValidatorRPC_test.cpp +++ b/src/test/rpc/ValidatorRPC_test.cpp @@ -134,6 +134,39 @@ public: auto const jrr = env.rpc("validator_list_sites")[jss::result]; BEAST_EXPECT(jrr[jss::validator_sites].size() == 0); } + // Negative UNL empty + { + auto const jrr = env.rpc("validators")[jss::result]; + BEAST_EXPECT(jrr[jss::NegativeUNL].isNull()); + } + // Negative UNL update + { + hash_set disabledKeys; + auto k1 = randomKeyPair(KeyType::ed25519).first; + auto k2 = randomKeyPair(KeyType::ed25519).first; + disabledKeys.insert(k1); + disabledKeys.insert(k2); + env.app().validators().setNegativeUnl(disabledKeys); + + auto const jrr = env.rpc("validators")[jss::result]; + auto& jrrnUnl = jrr[jss::NegativeUNL]; + auto jrrnUnlSize = jrrnUnl.size(); + BEAST_EXPECT(jrrnUnlSize == 2); + for (std::uint32_t x = 0; x < jrrnUnlSize; ++x) + { + auto parsedKey = parseBase58( + TokenType::NodePublic, jrrnUnl[x].asString()); + BEAST_EXPECT(parsedKey); + if (parsedKey) + BEAST_EXPECT( + disabledKeys.find(*parsedKey) != disabledKeys.end()); + } + + disabledKeys.clear(); + env.app().validators().setNegativeUnl(disabledKeys); + auto const jrrUpdated = env.rpc("validators")[jss::result]; + BEAST_EXPECT(jrrUpdated[jss::NegativeUNL].isNull()); + } } void