Skip to content
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

Burn fees collected into invalid accounts #33887

Merged
merged 8 commits into from
Nov 6, 2023

Conversation

jstarry
Copy link
Member

@jstarry jstarry commented Oct 27, 2023

Problem

Validators can currently collect transaction and rent fees into any account type they wish. This is unnecessarily permissive and fees should only be allowed to be collected into system owned accounts that are rent exempt.

Summary of Changes

  • Refactor: move fee distribution code into new bank::fee_distribution module to avoid increasing the size of bank.rs
  • Prevent collecting transaction and rent fees into non system owned accounts
  • Prevent the creation of new rent paying accounts via transaction fee collection
  • Move Bank::deposit into test_utils module to discourage its usage

Feature Gate Issue: #33888

@jstarry jstarry added the feature-gate Pull Request adds or modifies a runtime feature gate label Oct 27, 2023
@codecov
Copy link

codecov bot commented Oct 27, 2023

Codecov Report

Merging #33887 (9978d32) into master (ba112a0) will increase coverage by 0.0%.
Report is 29 commits behind head on master.
The diff coverage is 97.8%.

@@           Coverage Diff            @@
##           master   #33887    +/-   ##
========================================
  Coverage    81.8%    81.9%            
========================================
  Files         810      811     +1     
  Lines      218148   218368   +220     
========================================
+ Hits       178650   178952   +302     
+ Misses      39498    39416    -82     

Copy link
Contributor

@CriesofCarrots CriesofCarrots left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple questions and nits.
I only commented on a couple, but I'd love to see all the test match results cleaned up for readability.

runtime/src/bank/fee_distribution.rs Show resolved Hide resolved
runtime/src/bank.rs Show resolved Hide resolved
runtime/src/bank/fee_distribution.rs Outdated Show resolved Hide resolved
runtime/src/bank/fee_distribution.rs Outdated Show resolved Hide resolved
Comment on lines 174 to 183
warn!(
"Rent distribution of {rent_to_be_paid} to {pubkey} results in \
invalid RentState: {recipient_post_rent_state:?}"
);
datapoint_warn!(
"bank-rent_distribution_invalid_state",
("slot", self.slot(), i64),
("pubkey", pubkey.to_string(), String),
("rent_to_be_paid", rent_to_be_paid, i64)
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just calling out that we do lose this warning and datapoint in the new deposit_fees. Hopefully, prevent_rent_paying_rent_recipients() will be active before this PR is released, so we'll no longer care about tracking that specific case so carefully.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was curious if you wanted to keep that or not, I can keep it in if you want!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeffwashington , @brooksprumo , I think you're the main users of this datapoint currently. Thoughts on removing it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest patch of this PR removes this particular datapoint_warn but has two datapoint_warn's that track how many lamports were burned during both tx and rent fee distribution

datapoint_warn!(
    "bank-burned_rent",
    ("slot", self.slot(), i64),
    ("num_lamports", rent_to_burn, i64)
);

datapoint_warn!(
    "bank-burned_fee",
    ("slot", self.slot(), i64),
    ("num_lamports", deposit, i64),
    ("error", err.to_string(), String),
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdyt about adding the pubkey back into the bank-burned_fee datapoint?

I know that putting text/pubkeys into metrics isn't great, but it may be useful for any debugging. Since this is an exceptional case, I would expect this datapoint to not trigger often.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For bank-burned_fee we track the slot which allows us to infer who the leader was that failed to collect tx fees

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me, thanks!

Comment on lines 239 to 247
error!(
"Burned {} rent lamports instead of sending to {}",
rent_to_be_paid, pubkey
);
datapoint_error!(
"bank-burned_rent",
("slot", self.slot(), i64),
("num_lamports", rent_to_be_paid, i64)
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we maybe want to back down these, or at least the log? It's not really the kind of thing a node operator would interpret as an error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good idea. I think that if a validator is burning its own fees it should see an error but it won't care about other validators. Do you care if I remove the datapoint?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know of a need for the datapoint and am personally fine with its removal, but that might be a good question for @jeffwashington or @HaoranYi

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a datapoint_info! will be enough. I think it will be better if we accumulate all the burned fees and submit one data point for it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HaoranYi is there a specific reason why you would like to track this datapoint? I can't think of any reason personally.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It gives us the visibility of how much fees are burned in the network. The amount of the fees burned should be very small. To me, being able to run such a query and verify the amount of fees burned, could be a useful sanity check.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll add it back in, thanks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the datapoint back in but downgraded it to "warn"

runtime/src/bank/fee_distribution.rs Outdated Show resolved Hide resolved
@jstarry jstarry force-pushed the feature/check-fee-distribution branch from fbeea2e to 4cb3bca Compare October 30, 2023 13:07
}

// Deposits fees into a specified account and if successful, returns the new balance of that account
fn deposit_fees(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we split this fu into two fns? one for transaction fee specificially and one for rent specificially?
For transaction fee, we don't need to check for rent state transition. Right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the same checks (including rent state transition) in both cases, that's why I created this function. If we collect transaction fees into an account whose post balance is not greater than or equal to the rent exempt balance, we need to skip the fee deposit because otherwise we allow creating rent paying accounts. Make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah.
It seems that the current code still allows transaction fees to be collected into an account whose post balance is not greater than or equal to the rent exempt balance. Should we create a separate PR to fix this for current code and backport it to v1.17?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean, can you elaborate more? By the "current code" you mean the code in this PR right? And under what conditions are you saying that the code "still allows transaction fees to be collected into an account whose post balance is not greater than or equal to the rent exempt balance"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "current code", I mean master.

In master, we don't validate the post balance of the account that transaction fees are deposit to. If the balance of the account pre-transaction fee was zero, then after the transaction fee deposit, we introduce a "new" rent paying account, right?

The new code (i.e. deposit_fee fn) in the PR will prevent this from happening. What I mean is that we pull this piece out in a new PR to patch master separately.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I'd rather not break up apart this PR unless we have a good reason to. I purposely setup this PR to introduce the shared deposit_fee function to ensure that all invalid cases are handled for each type of fee collection. Before, without a common function, we missed the rent paying edge case for collected transaction fees.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I think we might want to port this feature back to 1.17 to close those edge cases as soon as possible.

Copy link
Contributor

@HaoranYi HaoranYi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. Just a few nits and suggestions.

CriesofCarrots
CriesofCarrots previously approved these changes Nov 3, 2023
Copy link
Contributor

@CriesofCarrots CriesofCarrots left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm. It just occurred to me that, since this adds a feature gate, it really should have been a SIMD first.

}
}
}
});
self.rewards.write().unwrap().append(&mut rewards);

if rent_to_burn > 0 {
self.capitalization.fetch_sub(rent_to_burn, Relaxed);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change to a single subtraction per loop looks fine, but since it was potentially substantive, it would have been nice if called out in its own commit.

runtime/src/bank.rs Outdated Show resolved Hide resolved
Comment on lines 174 to 183
warn!(
"Rent distribution of {rent_to_be_paid} to {pubkey} results in \
invalid RentState: {recipient_post_rent_state:?}"
);
datapoint_warn!(
"bank-rent_distribution_invalid_state",
("slot", self.slot(), i64),
("pubkey", pubkey.to_string(), String),
("rent_to_be_paid", rent_to_be_paid, i64)
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdyt about adding the pubkey back into the bank-burned_fee datapoint?

I know that putting text/pubkeys into metrics isn't great, but it may be useful for any debugging. Since this is an exceptional case, I would expect this datapoint to not trigger often.

@jstarry
Copy link
Member Author

jstarry commented Nov 5, 2023

Lgtm. It just occurred to me that, since this adds a feature gate, it really should have been a SIMD first.

Good point, I created a SIMD to improve visibility and explain this change in more detail: solana-foundation/solana-improvement-documents#85

Copy link
Contributor

@HaoranYi HaoranYi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@jstarry jstarry merged commit ebe8afb into solana-labs:master Nov 6, 2023
42 checks passed
@jstarry jstarry deleted the feature/check-fee-distribution branch November 6, 2023 02:02
@jstarry jstarry added the v1.17 PRs that should be backported to v1.17 label Nov 6, 2023
mergify bot pushed a commit that referenced this pull request Nov 6, 2023
* refactor: create bank::fee_distribution module

* feature: add checks to fee distribution

* refactor: move Bank::deposit fn into test_utils

* feedback

* feedback 2

* add datapoints

* change to datapoint_warn

* typo

(cherry picked from commit ebe8afb)

# Conflicts:
#	runtime/src/bank.rs
#	runtime/src/bank/tests.rs
jstarry added a commit that referenced this pull request Nov 6, 2023
…#33951)

* Burn fees collected into invalid accounts (#33887)

* refactor: create bank::fee_distribution module

* feature: add checks to fee distribution

* refactor: move Bank::deposit fn into test_utils

* feedback

* feedback 2

* add datapoints

* change to datapoint_warn

* typo

(cherry picked from commit ebe8afb)

# Conflicts:
#	runtime/src/bank.rs
#	runtime/src/bank/tests.rs

* resolve conflicts

---------

Co-authored-by: Justin Starry <justin.starry@icloud.com>
Co-authored-by: Justin Starry <justin@solana.com>
Copy link
Contributor

@t-nelson t-nelson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that the refactors have been squashed into the logical changes is pretty not great. this should have been at least two PRs, probably more.

have we checked how many mb validators will be effected by this change?

do we intend to prevent assignment of invalid fee-collection accounts?

@@ -3805,7 +3804,7 @@ impl Bank {
.stakes_cache
.stakes()
.highest_staked_node()
.unwrap_or_default();
.unwrap_or_else(Pubkey::new_unique);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method should absolutely not be used outside of tests under any circumstances. can we just fail/panic instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only used in tests right now... using the system program (default pubkey) as the highest staked node should also not happen outside of tests in any circumstances. I'll clean it up in a follow up change though to be sure

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR fix is being worked on here: #34135

fees: u64,
options: DepositFeeOptions,
) -> Result<u64, DepositFeeError> {
let mut account = self.get_account_with_fixed_root(pubkey).unwrap_or_default();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not fail directly here rather than do the whole footgun-prone unwrap_or_default() thing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's fine if the account doesn't exist as long as the fees top up the balance to at least rent exempt.

@jstarry
Copy link
Member Author

jstarry commented Nov 16, 2023

that the refactors have been squashed into the logical changes is pretty not great. this should have been at least two PRs, probably more.

Noted, will do that next time.

have we checked how many mb validators will be effected by this change?

Right now the validator implementation forces nodes to use the fee collector account (also known as node id account) as the fee payer for their votes (therefore must already be a valid system account). And tpu votes also checks that the node id is the payer for the votes and uses that to weight vote transactions for block inclusion. So no one will be impacted.

do we intend to prevent assignment of invalid fee-collection accounts?

Not now.. it isn't possible to enforce that a fee-collection account stays valid. Its owner can change at any time and it can be withdrawn to zero lamports at any time. Best thing I think we could do in the future is to create an enshrined "NodeAccount" or "FeeCollectorAccount" that must always be valid and used separately from fee payments. But that's a big lift and probably not necessary. If validators want fees, they won't create funky fee collection accounts.

anwayde pushed a commit to firedancer-io/solana that referenced this pull request Nov 16, 2023
…labs#33887) (solana-labs#33951)

* Burn fees collected into invalid accounts (solana-labs#33887)

* refactor: create bank::fee_distribution module

* feature: add checks to fee distribution

* refactor: move Bank::deposit fn into test_utils

* feedback

* feedback 2

* add datapoints

* change to datapoint_warn

* typo

(cherry picked from commit ebe8afb)

# Conflicts:
#	runtime/src/bank.rs
#	runtime/src/bank/tests.rs

* resolve conflicts

---------

Co-authored-by: Justin Starry <justin.starry@icloud.com>
Co-authored-by: Justin Starry <justin@solana.com>
@t-nelson
Copy link
Contributor

this was way too big to backport. cc/ @willhickey

if we're developing a fix with intention to backport, it must be done in the smallest change set possible. now we have a mass of refactor to sift through if we need to debug compatibility issues (which we are ATM)

anwayde pushed a commit to firedancer-io/solana that referenced this pull request Nov 16, 2023
…labs#33887) (solana-labs#33951)

* Burn fees collected into invalid accounts (solana-labs#33887)

* refactor: create bank::fee_distribution module

* feature: add checks to fee distribution

* refactor: move Bank::deposit fn into test_utils

* feedback

* feedback 2

* add datapoints

* change to datapoint_warn

* typo

(cherry picked from commit ebe8afb)

# Conflicts:
#	runtime/src/bank.rs
#	runtime/src/bank/tests.rs

* resolve conflicts

---------

Co-authored-by: Justin Starry <justin.starry@icloud.com>
Co-authored-by: Justin Starry <justin@solana.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-gate Pull Request adds or modifies a runtime feature gate v1.17 PRs that should be backported to v1.17
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants