-
Notifications
You must be signed in to change notification settings - Fork 646
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Excessive copying of ValidatorStake in epoch manager #2610
Comments
CC @mikhailOK , @bowenwang1996 |
Moving discussion from Slack:
|
We can avoid cloning, but still have to do aggregation at some point, which will still result in slowness. Just for keeping these many proposals in memory will cost around 460MB, which may be fine. But the computation for validator set will likely be slower than acceptable. |
That is what we did before. See my comment above. |
To avoid over optimization I think we can do aggregation at the end of the epoch like what we did before, but the current cost might need to be adjusted so that the aggregation and the following computation wouldn't be too slow. I'll do some benchmark to see what the appropriate number of proposals is. |
The stake return at the end of the epoch has to touch all accounts that made proposals, and it's not possible to optimize because it changes state. |
This does not seem to be the bottleneck right now.
We can do aggregation on the database side. Our aggregation is very simple and it is very similar to what indexes in the databases do. There is not need to load all proposals into memory at the end of the epoch, if we figure out smart way of writing proposals on each block into the database then epoch aggregation can be a simple database query. Rocksdb however does not support queries so see my explanation below, but I think we can pull it off anyway. Here is how we can solve it if there were no forks:
Properties of this approach:
Can we make it work with forks? |
We still need to defend from this attack |
I agree. |
@nearmax I think I didn't fully understand your approach.
How does this prevent taking multiple proposals from the same account? We only take one per account. Also we always take the last proposal whereas that is not preserved in your approach. EDIT: it might work if we switch to using |
@mikhailOK good point. |
Even better! We then create another column (B) with key/value: and . When we process the block as we iterate over proposals in the block we extract and remove entry The original column (A) that would need to have key: |
@nearmax looks like something is missing here
Also I don't see how that works with forks. Optimizations aside, it seems that the main problem right now is the amount of the state updates we potentially need to do. I am curious how many accounts can be updated in 1 second. It is probably related to the size and shape of the trie. |
Up to 2000 -- that's what our TPS with financial transactions. |
Staking pool contract potentially wants to send a lot of proposals from one account, our defense from million proposal attack should not ban that case |
After discussion with @evgenykuzyakov, @nearmax, @mikhailOK, @ilblackdragon, we decided to use the following approach: say the seat price computed at the end of epoch X-1 (for epoch X+1) is P, then during epoch X, we reject all staking transactions whose stake is less than P/N where N is a system parameter. The idea is that the seat price wouldn't change too drastically between epochs and we can leverage that to reject staking transactions early. Now we need to decide on the value on N (10 was proposed). |
Additionally, all |
@ilblackdragon we need to store slashed validators at the tip. |
#2805) …uirements Fixes #2610 in two parts: 1. Introduce a `minimum_stake_divisor` to have a lower limit for staking transactions. If someone tries to stake with less than `last_seat_price / minimum_stake_divisor`, the transaction will fail. 2. Instead of copying the proposals in every block info, use an aggregator that always updates the proposal information to the last final block and at the end of an epoch, process the rest between last final block and last block of the epoch. Test plan --------- * Unit tests in epoch manager to make sure that the change in how we store aggregated information doesn't break anything. * Tests in `neard/src/runtime` to make sure that the proposals that are now invalid are properly rejected.
Currently, every time epoch manager processes block X it produces
BlockInfo
and copies all proposals fromBlockInfo
for block X-1 intoBlockInfo
for block X. It creates an accumulation of cloned proposals with each new block processing, until the end of the epoch. In other words, the proposal submitted in the first block of the epoch will be cloned for each block in that epoch. See the cloning code here: https://github.com/nearprotocol/nearcore/blob/master/chain/epoch_manager/src/lib.rs#L431This creates a security vulnerability. If someone submits staking transactions from different accounts with 1000 transactions per block, it will take 227 blocks until the total number of cloned memory exceeds 4GiB, since a single
ValidatorStake
can take up to 112 bytes. Since our current cache allows 1000 blocks this attack becomes feasible, see: https://github.com/nearprotocol/nearcore/blob/7c75fe5bc2eb44ae08a3445116157ac8fa77f2ac/chain/epoch_manager/src/lib.rs#L31Simple solutions that wouldn't work
Reducing cache size
We could try reducing the cache size, but even if we had not cache at all, the node would break simply from cloning huge amounts of memory, on each new block. Specifically, after 5 hours (assuming we process 1 block per second) we would be copying 1.8GiB per block. Just having two consecutive
BlockInfo
s would take almost 4GiB of memory.Increasing the cost of staking transaction
If we assume that it is safe to copy 100MiB of proposals on each block processing, and epoch consists of 3600*12 blocks, we should raise the fees to prohibit more than 22 staking transactions per block. This number seems to be very low.
The proposed solution
I suggest to redesign epoch manager to avoid cloning of the
ValidatorStake
altogether.The text was updated successfully, but these errors were encountered: