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

wallet2: validate fetched block height and parent hash #9344

Merged
merged 2 commits into from
Jul 16, 2024

Conversation

valldrac
Copy link
Contributor

This PR adds integrity checks in wallet2 to ensure that blocks fetched from remote nodes during the refresh process align correctly with the wallet's local view of the blockchain.

It mitigates some of the risks described in the security advisory (link) regarding the trust in remote nodes for blockchain synchronization, without needing to verify the PoW.

Currently, wallet2 allows malicious nodes to provide a forged (non-mined) block at a specific height when requested by the wallet, while providing genuine blocks thereafter. All this without triggering a blockchain split, maintaining the appearance of the real blockchain, and avoiding later detection. This is possible because wallet2 fails to verify whether each block's parent hash (field prev_id in the block header) matches the hash of the previous block in the chain.

With these new basic checks, any fake block injected will be detected upon connection to an honest node. This detection will trigger either a reorganization (within the max-reorg-depth limit) or a fatal error to prevent further compromise.

Note that any injection attack that occurred prior to this PR can only be detected by issuing a rescan of the wallet.

@selsta
Copy link
Collaborator

selsta commented May 28, 2024

Does this have an impact on wallet scan speed?

@valldrac
Copy link
Contributor Author

Does this have an impact on wallet scan speed?

No, it doesn't affect performance at all.

@selsta
Copy link
Collaborator

selsta commented May 29, 2024

@valldrac
Copy link
Contributor Author

@valldrac some tests failed, can you take a look? https://github.com/monero-project/monero/actions/runs/9274388707/job/25517494248?pr=9344

Fixed. I had assumed process_parsed_blocks() was never called with start_height=0, but it seems the node always returns the latest matching block, which can include the genesis block.

@valldrac
Copy link
Contributor Author

valldrac commented Jul 3, 2024

Hi. Just a friendly ping to request a review and merge for this PR. It addresses an important issue with the blockchain integrity during refresh in wallet2. No impact on wallet scan speed, and tests have passed.

@@ -3110,11 +3114,10 @@ void wallet2::pull_hashes(uint64_t start_height, uint64_t &blocks_start_height,
//----------------------------------------------------------------------------------------------------
void wallet2::process_parsed_blocks(uint64_t start_height, const std::vector<cryptonote::block_complete_entry> &blocks, const std::vector<parsed_block> &parsed_blocks, uint64_t& blocks_added, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache)
{
size_t current_index = start_height;
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand why you moved current_index to where it's used the first time, but this is an unrelated change for this PR. As a result, half of the changes in this function are unnecessary.

Patches should be self contained. A good rule of thumb is to have one patch per separate issue, feature, or logical change. Also, no other changes, such as random whitespace changes, reindentation, or fixing typos, spelling, or wording, unless user visible. Following the code style of the particular chunk of code you're modifying is encouraged. Proper squashing should be done (eg, if you're making a buggy patch, then a later patch to fix the bug, both patches should be merged).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe this change is necessary to prevent future issues. I had to choose between two variables, neither of which was correct for my PR needs. is_in_bounds(start_height) must be called before start_height is used, but it was accessed via current_index, which is mutated later in the function. This mix-up could lead to future issues.

Such a trivial change is easy to review and quick to fix in any merge conflict. Leaving it as is would be incorrect, because the current code already mixes mutable and immutable variables. Probably reviewing the commit history would show that this mix-up comes from previous merges that made minimal changes without considering the context.

I can split the commit in two to clarify the change if necessary, but creating another PR only for this seems excessive.

Copy link
Contributor

Choose a reason for hiding this comment

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

Splitting into two commits is fine. And while you're at it, start_height should then be declared as const uint64_t in function definition and declaration, if it's not supposed to be changed in the function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I've split into two commits and declared start_height as const.

@SChernykh
Copy link
Contributor

Currently, wallet2 allows malicious nodes to provide a forged (non-mined) block at a specific height when requested by the wallet, while providing genuine blocks thereafter.

What prevents a malicious node from forging prev_id field too? Does wallet2 check the actual hash of the block somewhere else? I don't see calls to any hashing functions in this PR.

@valldrac
Copy link
Contributor Author

valldrac commented Jul 4, 2024

What prevents a malicious node from forging prev_id field too? Does wallet2 check the actual hash of the block somewhere else? I don't see calls to any hashing functions in this PR.

Yes, wallet2 already hashes the block internally when it calls cryptonote::parse_and_validate_block_from_blob() from pull_and_parse_next_blocks(). After that, it uses this hash value as the block ID consistently. So, if a node tries to change prev_id field, it would alter the block ID too.

@valldrac valldrac force-pushed the validate-block-parent-hash branch from 4d6f781 to 377445c Compare July 4, 2024 11:03
@@ -3125,8 +3128,20 @@ void wallet2::process_parsed_blocks(uint64_t start_height, const std::vector<cry
num_txes += 1 + parsed_blocks[i].txes.size();
tx_cache_data.resize(num_txes);
size_t txidx = 0;
crypto::hash prev_block_id;
if (start_height > 0) {
prev_block_id = m_blockchain[start_height - 1];
Copy link
Contributor

Choose a reason for hiding this comment

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

There is a check above that start_height is in bounds, but you never check that start_height - 1 is in bounds. It must be checked too.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess you just shouldn't init prev_block_id if it's not in bounds, instead of throwing an exception like above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right. It could fail if the blockchain is trimmed to one block. I just fixed it.

@valldrac valldrac force-pushed the validate-block-parent-hash branch from 377445c to a026d5a Compare July 5, 2024 03:49
@selsta
Copy link
Collaborator

selsta commented Jul 5, 2024

@tevador any opinion on this?

Copy link
Contributor

@jeffro256 jeffro256 left a comment

Choose a reason for hiding this comment

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

Looks good to me!

@luigi1111 luigi1111 merged commit c2fceb2 into monero-project:master Jul 16, 2024
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants