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

chain: don't skip early transaction validity checks in state witnesses #12627

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions chain/chain/src/stateless_validation/chunk_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ impl MainTransition {
pub struct PreValidationOutput {
pub main_transition_params: MainTransition,
pub implicit_transition_params: Vec<ImplicitTransitionParams>,
/// List of the transactions that are valid and should be processed by e.g.
/// `validate_chunk_state_witness`.
///
/// This list is exactly the length of the corresponding `ChunkStateWitness::transactions`
/// field. Element at the index N in this array corresponds to an element at index N in the
/// transactions list.
///
/// Transactions for which a `false` is stored here ought to be ignored/dropped/skipped.
///
/// All elements will be true for protocol versions where `RelaxedChunkValidation` is not
/// enabled.
pub transaction_validity_check_passed: Vec<bool>,
}

#[derive(Clone)]
Expand Down Expand Up @@ -369,11 +381,20 @@ pub fn pre_validate_chunk_state_witness(

let current_protocol_version =
epoch_manager.get_epoch_protocol_version(&state_witness.epoch_id)?;
if !checked_feature!(
let transaction_validity_check_passed = if checked_feature!(
"protocol_feature_relaxed_chunk_validation",
RelaxedChunkValidation,
current_protocol_version
) {
if last_chunk_block.header().is_genesis() {
vec![true; state_witness.transactions.len()]
Comment on lines +389 to +390
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had to add this to make test that start at the genesis to pass. There is no "prev_block_header" in this instance. The code further below that relies on the previous block has a similar special case for the genesis.

This vector currently ends up not being used, but if it did, it seemed to me that considering all transactions "valid" is reasonable enough: the transactions can't be much older than any useful transaction_validity_period. On the other hand if there are additional checks added to transaction_validity_check in the future, this might no longer hold… So this is not a particularly resilient piece of code in my eyes.

} else {
let prev_block_header =
store.get_block_header(last_chunk_block.header().prev_hash())?;
let mut check = chain.transaction_validity_check(prev_block_header);
state_witness.transactions.iter().map(|t| check(t)).collect::<Vec<_>>()
}
} else {
let new_transactions = &state_witness.new_transactions;
let (new_tx_root_from_state_witness, _) = merklize(&new_transactions);
let chunk_tx_root = state_witness.chunk_header.tx_root();
Expand Down Expand Up @@ -420,7 +441,8 @@ pub fn pre_validate_chunk_state_witness(
}
};
}
}
vec![true; state_witness.transactions.len()]
};

let main_transition_params = if last_chunk_block.header().is_genesis() {
let epoch_id = last_chunk_block.header().epoch_id();
Expand Down Expand Up @@ -462,7 +484,11 @@ pub fn pre_validate_chunk_state_witness(
})
};

Ok(PreValidationOutput { main_transition_params, implicit_transition_params })
Ok(PreValidationOutput {
main_transition_params,
implicit_transition_params,
transaction_validity_check_passed,
})
}

/// Validate that receipt proofs contain the receipts that should be applied during the
Expand Down Expand Up @@ -611,7 +637,21 @@ pub fn validate_chunk_state_witness(
let (mut chunk_extra, mut outgoing_receipts) =
match (pre_validation_output.main_transition_params, cache_result) {
(MainTransition::Genesis { chunk_extra, .. }, _) => (chunk_extra, vec![]),
(MainTransition::NewChunk(new_chunk_data), None) => {
(MainTransition::NewChunk(mut new_chunk_data), None) => {
let mut validity_iterator =
pre_validation_output.transaction_validity_check_passed.iter();
new_chunk_data.transactions.retain(|t| {
let valid = *validity_iterator.next().unwrap();
if !valid {
tracing::debug!(
target: "chain",
message="discarding invalid transaction",
tx=%t.get_hash()
);
}
valid
});

let chunk_header = new_chunk_data.chunk_header.clone();
let NewChunkResult { apply_result: mut main_apply_result, .. } = apply_new_chunk(
ApplyChunkReason::ValidateChunkStateWitness,
Copy link
Contributor

@jancionear jancionear Dec 17, 2024

Choose a reason for hiding this comment

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

I have one more concern - this modifies the code for applying a chunk during state witness validation, but a chunk can also be applied in other ways. Nodes that track this shard will fetch the chunk and apply it independently, but I'm not sure if they'll filter the transactions in the same way. Applying a chunk with a different set of transactions could produce different results and the consensus could diverge.
Maybe it would be better to filter transactions in process_transactions during chunk application?

Copy link
Collaborator Author

@nagisa nagisa Dec 17, 2024

Choose a reason for hiding this comment

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

I was trying to implement exactly that at first, but this code is very much removed from the Chain data which is required to validate the transaction validity period.

Do you think it would be better to thread chain all the way to the bowels of the transaction runtime?

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think it would be better to thread chain all the way to the bowels of the transaction runtime?

It'd be more foolproof, but I don't know how hard that is to do. We already have EpochInfoProvider in the runtime, maybe we could add a ChainInfoProvider as well?

Copy link
Collaborator Author

@nagisa nagisa Dec 20, 2024

Choose a reason for hiding this comment

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

So, I experimented with this over the past week -- it is pretty darn difficult to implement something similar to EpochInfoProvider. First complicating factor is that in many cases with our current code structure the processing requires : Send + Sync or 'static on this conceptual ChainProvider. To get the minimal required such ChainProvider be 'static, we could put the Chain::chain_store into an Arc and move that around. But then this Chain::chain_store is accessed both mutably and immutably. So it also requires a Mutex.

From there a lot of our code is written assuming a) cheap access to ChainStore; b) to hold relatively long-lived references to ChainStore. So what would seem like initially a straightforward change suddenly becomes a big snowball rolling off a hillside.

I have a half-done prototype here: master...nagisa:nearcore:chain-provider-style-tx-validation with the main missing pieces being: a) updates to ChainStoreUpdate to work with Arc<Mutex<ChainStore>>; b) actual invocation of the newly added validation methods; c) some compile errors here and there, some todo!()s in places not related to state witness today which need additional threading through of data.

Now, I largely just steamrolled through the change here -- perhaps there is some refactor to chain that would be broadly beneficial and enable this change as well. But I wasn't able to spot anything of the sort with my limited knowledge of the chain code. Maybe you see a reason why it would be better to go with something like this still?

Copy link
Collaborator Author

@nagisa nagisa Dec 20, 2024

Choose a reason for hiding this comment

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

I have updated the PR above to cargo check successfully. This means that I have threaded the ChainProvider through to the transaction runtime (Runtime::apply) from all callers.

There are five separate major call-points in regular code flow where transaction runtime has forced to thread ChainProvider through.

  • Chain::get_update_shard_job -- internally validates transactions (calls validate_chunk_transactions;)
  • stateless validation -- addressed here;
  • Chain::set_state_finalize -- does not operate on untrusted chunks(?);
  • catchup/state sync -- did not verify transaction validity before state witnesses and continues not validating them.
  • tests.

Out of these the catchup code smells most prone to problems here. Other than that everything else is in test code, utilities and tools.

Expand Down
Loading