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

Refactor/Simplify validation logic #1847

Merged
merged 63 commits into from
Apr 29, 2024

Conversation

MitchTurner
Copy link
Member

@MitchTurner MitchTurner commented Apr 19, 2024

Part of: #1751

Have validation functions just take a Block rather than components. Remove Validation variant of ExecutionKind. Clean up a bunch of other little details by removing generalizations that existed before for things like tx source.

A lot of these changes are breaking down huge functions and trying to make it a bit more declarative. This will hopefully mitigate the need for hand-holding comments all the way down and you can just read the name of the functions to know what is going on. For example:

        // Throw a clear error if the transaction id is a duplicate
        if tx_st_transaction
            .storage::<ProcessedTransactions>()
            .contains_key(tx_id)?
        {
            return Err(ExecutorError::TransactionIdCollision(*tx_id))
        }

became

Self::check_tx_is_not_duplicate(tx_id, tx_st_transaction)?;

about 20x better.

I'm still kinda unsatisfied with the clarity with some spots though. For example, I'm not sure what state is supposed to change in the case of a revert, I'm just trusting the order things existed before and the tests. I think it could be better to have a match case for if reverted { ... } else { ... } instead of threading the reverted bool through to subsequent functions.

Checklist

  • Breaking changes are clearly marked as such in the PR description and changelog
  • New behavior is reflected in tests
  • The specification matches the implemented behavior (link update PR if changes are needed)

Before requesting review

  • I have reviewed the code myself
  • I have created follow-up issues caused by this PR and linked them here

After merging, notify other teams

[Add or remove entries as needed]

@MitchTurner MitchTurner force-pushed the refactor/remove-production-logic-from-validation branch from 4e4be31 to 6e1762e Compare April 19, 2024 23:42
@MitchTurner MitchTurner changed the title Refactor/remove production logic from validation Refactor/Simplify validation logic Apr 19, 2024
Comment on lines 678 to 681
#[cfg(debug_assertions)]
if _new_transaction != transaction {
error!("Provided transaction does not match the transaction built by the executor");
}
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 I really like this. It's kinda chatty in the tests. Is there another approach we could take? We can't have it throw an error because the tests are expecting the error that will be used in prod.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of comparing transactions here, we can do that inside of the check_block_matches. You can even iterate over the block's transactions there and compare transactions and have a behavior like before the change(return InvalidTransactionOutcome)=)

/// UTXO Validation flag, when disabled the executor skips signature and UTXO existence checks
pub utxo_validation: bool,
pub extra_tx_checks: bool,
Copy link
Member Author

Choose a reason for hiding this comment

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

This should simply be named after the situation we want to use it. I'm not sure what it is yet. For now, I just named it extra_tx_checks which is vague, but utxo_validation was incorrect, so it's an improvement.

@MitchTurner MitchTurner requested review from a team and xgreenx April 25, 2024 00:17
Copy link
Collaborator

@xgreenx xgreenx left a comment

Choose a reason for hiding this comment

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

Almost ideal!=)

Comment on lines 384 to 389
debug!(
"Block {:#x} fees: {} gas: {}",
pre_exec_block_id.unwrap_or(finalized_block_id),
coinbase,
used_gas
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we still want to have debug log here=)

Copy link
Collaborator

Choose a reason for hiding this comment

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

We want to have one log for the executor and one log for validator

&mut tx_st_transaction,
)?;
tx_st_transaction.commit()?;
tx
};

block.transactions.push(tx);
block.transactions.push(tx.clone());
Copy link
Collaborator

Choose a reason for hiding this comment

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

It may be too expensive to clone transaction. You could return a reference to it. Like block.transactions.as_ref().expect("We just added transaction") 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.

Turns out we don't need it because we are inspecting the txs after.

Comment on lines 636 to 637
let mut partial_block = PartialFuelBlock::from(block.clone());
let transactions = core::mem::take(&mut partial_block.transactions);
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be nice if you avoided cloning of the block here=) You don't need to modify transactions, so you can work with the reference

Comment on lines 678 to 681
#[cfg(debug_assertions)]
if _new_transaction != transaction {
error!("Provided transaction does not match the transaction built by the executor");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of comparing transactions here, we can do that inside of the check_block_matches. You can even iterate over the block's transactions there and compare transactions and have a behavior like before the change(return InvalidTransactionOutcome)=)

Comment on lines 1148 to 1170
fn check_mint_amount(mint: &Mint, expected_amount: u64) -> ExecutorResult<()> {
if *mint.mint_amount() != expected_amount {
return Err(ExecutorError::CoinbaseAmountMismatch)
}
Ok(())
}

let vm_result: StateTransition<_> = vm
.transact(ready_tx)
.map_err(|error| ExecutorError::VmExecution {
error: error.to_string(),
transaction_id: tx_id,
})?
.into();
let reverted = vm_result.should_revert();
fn check_gas_price(mint: &Mint, expected_gas_price: Word) -> ExecutorResult<()> {
if *mint.gas_price() != expected_gas_price {
return Err(ExecutorError::CoinbaseGasPriceMismatch)
}
Ok(())
}

let (state, mut tx, receipts): (_, Tx, _) = vm_result.into_inner();
#[cfg(debug_assertions)]
{
tx.precompute(&self.consensus_params.chain_id())?;
debug_assert_eq!(tx.id(&self.consensus_params.chain_id()), tx_id);
fn check_mint_has_expected_index(
checked_mint: &Checked<Mint>,
execution_data: &ExecutionData,
) -> ExecutorResult<()> {
if checked_mint.transaction().tx_pointer().tx_index() != execution_data.tx_count {
return Err(ExecutorError::MintHasUnexpectedIndex)
}
Ok(())
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think adding 3 new methods, 3 lines each, is overkill since the if statement and return error type are already descriptive enough.

Could you return it back, please=)

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 strongly prefer the new code. I think the reduction of noise and keeping the caller function as flat as possible is worth the cost of maintaining another function. These benefits add up quickly and I'd like to go farther tbh but I think it's time to wrap up this PR.

}

#[allow(clippy::too_many_arguments)]
fn execute_chargeable_transaction<Tx, T>(
fn execute_mint_with_vm<T>(
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems this new function shifted execute_chargeable_transaction below and the change diff looks bad now. Could you swap execute_mint_with_vm with execute_chargeable_transaction please for the history sake

block_st_transaction,
&coinbase_id,
&mut mint,
&mut inputs,
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be nice if you pass input instead=)

tx_st_transaction: &mut StorageTransaction<T>,
) -> ExecutorResult<Transaction>
where
Tx: ExecutableTransaction + PartialEq + Cacheable + Send + Sync + 'static,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Tx: ExecutableTransaction + PartialEq + Cacheable + Send + Sync + 'static,
Tx: ExecutableTransaction + Cacheable + Send + Sync + 'static,

let fee_params = self.consensus_params.fee_params();

let ready_tx = checked_tx
.clone()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Now(because we don't need to compare checked_tx with tx) you can create a vector of predicate_gas_used from inputs and then you don't need to checked_tx.clone()=)

@@ -1349,7 +1263,35 @@ where
tx.outputs(),
)?;

// We always need to update outputs with storage state after execution.
self.update_tx_outputs(tx_st_transaction, tx_id, &mut tx)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know that it was before like this, but it would be nice if you moved this function to the attempt_tx_execution_with_vm. Then we can remove mut part from the tx=)

Copy link
Collaborator

@xgreenx xgreenx left a comment

Choose a reason for hiding this comment

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

Looks perfect to me=) Would be nice if @Dentosal also reviewed it=)

block_st_transaction,
inputs.as_mut_slice(),
std::slice::from_ref(&input),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
std::slice::from_ref(&input),
core::slice::from_ref(&input),

let mut sub_block_db_commit = block_st_transaction
.write_transaction()
.with_policy(ConflictPolicy::Overwrite);
self.compute_inputs(std::slice::from_mut(&mut input), block_st_transaction)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
self.compute_inputs(std::slice::from_mut(&mut input), block_st_transaction)?;
self.compute_inputs(core::slice::from_mut(&mut input), block_st_transaction)?;


let block_height = *header.height();
Self::check_mint_amount(&mint, execution_data.coinbase)?;
Self::check_gas_price(&mint, gas_price)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we need to move this check before if statement, otherwise in the case of zero CotnractId it is possible to lie about the gas price=)

Copy link
Member Author

Choose a reason for hiding this comment

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

tx.outputs(),
coinbase_id,
block_st_transaction,
std::slice::from_ref(input),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
std::slice::from_ref(input),
core::slice::from_ref(input),

)?;
self.compute_state_of_not_utxo_outputs(
outputs.as_mut_slice(),
std::slice::from_ref(input),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
std::slice::from_ref(input),
core::slice::from_ref(input),

@MitchTurner MitchTurner requested review from Dentosal and a team April 26, 2024 17:36
@xgreenx xgreenx merged commit e044578 into master Apr 29, 2024
33 checks passed
@xgreenx xgreenx deleted the refactor/remove-production-logic-from-validation branch April 29, 2024 11:30
@xgreenx xgreenx mentioned this pull request Apr 30, 2024
xgreenx added a commit that referenced this pull request Apr 30, 2024
## Version v0.26.0

### Fixed

#### Breaking

- [#1868](#1868): Include the
`event_inbox_root` in the header hash. Changed types of the
`transactions_count` to `u16` and `message_receipt_count` to `u32`
instead of `u64`. Updated the application hash root calculation to not
pad numbers.
- [#1866](#1866): Fixed a
runtime panic that occurred when restarting a node. The panic happens
when the relayer database is already populated, and the relayer attempts
an empty commit during start up. This invalid commit is removed in this
PR.
- [#1871](#1871): Fixed
`block` endpoint to return fetch the blocks from both databases after
regenesis.
- [#1856](#1856): Replaced
instances of `Union` with `Enum` for GraphQL definitions of
`ConsensusParametersVersion` and related types. This is needed because
`Union` does not support multiple `Version`s inside discriminants or
empty variants.
- [#1870](#1870): Fixed
benchmarks for the `0.25.3`.
- [#1870](#1870): Improves the
performance of getting the size of the contract from the
`InMemoryTransaction`.
- [#1851](#1851): Provided
migration capabilities (enabled addition of new column families) to
RocksDB instance.

### Added 

- [#1853](#1853): Added a test
case to verify the database's behavior when new columns are added to the
RocksDB database.
- [#1860](#1860): Regenesis
now preserves `FuelBlockIdsToHeights` off-chain table.

### Changed

- [#1847](#1847): Simplify the
validation interface to use `Block`. Remove `Validation` variant of
`ExecutionKind`.
- [#1832](#1832): Snapshot
generation can be cancelled. Progress is also reported.
- [#1837](#1837): Refactor the
executor and separate validation from the other use cases

## What's Changed
* Weekly `cargo update` by @github-actions in
#1850
* Refactor/separate validation from other executions by @MitchTurner in
#1837
* fix: Use `Enum` for `ConsensusParametersVersion` and related types by
@bvrooman in #1856
* feat: snapshot generation graceful shutdown by @segfault-magnet in
#1832
* regenesis: migrate FuelBlockIdsToHeights by @Dentosal in
#1860
* Weekly `cargo update` by @github-actions in
#1869
* Refactor/Simplify validation logic by @MitchTurner in
#1847
* Fixed `block` endpoint to return fetch the blocks from both databases
after regenesis by @xgreenx in
#1871
* Add Eq and Partial Eq to tx response and status by @MujkicA in
#1872
* test: restart with relayer data by @bvrooman in
#1866
* Fix `BlockHeader` hash by @MitchTurner in
#1868
* Added a test for the case of adding new columns into the existing
RocksDB database by @xgreenx in
#1853
* Fixed benchmarks for the `0.25.3` by @xgreenx in
#1870


**Full Changelog**:
v0.25.3...v0.26.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants