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

Implemented update of balance_root and state_root for inputs and outputs #789

Merged
merged 12 commits into from
Dec 6, 2022

Conversation

xgreenx
Copy link
Collaborator

@xgreenx xgreenx commented Nov 25, 2022

The change actualizes the balance_root and state_root for inputs before execution and outputs after execution. It only preparation of the code to do those updates with the current dummy algorithm. The correct algorithm for state calculation should be implemented in #627 and #626.

Also, it is preparation for updating tx_pointer. Because it requires an additional field in the database, it should be done in a separate PR.

Minor changes:

CC @bvrooman This change should answer your question in the related issues.

@xgreenx xgreenx requested a review from a team November 25, 2022 17:24
@xgreenx xgreenx self-assigned this Nov 25, 2022
return Err(Error::InvalidTransactionOutcome {
transaction_id: tx.id(),
})
// TODO: Maybe we need move it to `fuel-vm`? O_o Because other `Outputs` are processed there
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'm unsure about this part. fuel-vm already updates outputs for all utxos. Maybe better to move it there or extract all updates here.

Copy link
Member

@Voxelot Voxelot Nov 29, 2022

Choose a reason for hiding this comment

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

fuel-vm could set the balance and state roots, but it doesn't manage utxo id's or utxo state directly. This is mainly to enable the client to do bulk optimization for utxo lookups and validation across many txs at once in parallel.

edit: however in the case of contract output utxo ids, the vm should be able to calculate those as well without interacting with any db state, so moving this to the VM may be the right call (and would probably make fraud proof setup easier).

.outputs_mut()
.last_mut()
{
*to = Address::try_from(contract_id.as_ref()).unwrap();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Voxelot I added this test but not sure what our expectation is. Maybe we want to reject transactions that try sending tokens to the contract via outputs(not the script body).

Copy link
Member

@Voxelot Voxelot Dec 6, 2022

Choose a reason for hiding this comment

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

Hmm yeah, we could do this but it would add extra overhead to every transfer to check if the output address is a contract id.

cc @adlerjohn

/// The wrapper around `contract_id` to simplify work with `Contract` in the database.
pub struct ContractRef<'a, Database> {
database: Database,
contract_id: &'a ContractId,
Copy link
Member

Choose a reason for hiding this comment

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

do we really need to lifetime reference this? the ContractId implements copy and should be fairly cheap to own here 🤔

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 added this type to simplify usage without affecting performance. Because the type is public and used inside of another crate, the compiler will not optimize usage of this struct, and it will add a smaaaaaaaaaaaaaall overhead(to copy 32 bytes)=)

If this lifetime causes pain to your eyes, I will remove it=D

Copy link
Contributor

Choose a reason for hiding this comment

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

While copying does incur a small overhead, dereferencing is also not free from a cache standpoint. I also think the simpler struct definition we get may be worth the cost of copying the Bytes32 .

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 don't know why we are afraid of lifetimes=) But if two people don't like it, I will replace it with four copies.

Copy link
Member

Choose a reason for hiding this comment

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

I know it's more efficient to use the lifetimes, but my preference is to try and keep the code as simple as possible until it's proven to be a bottleneck.

Copy link
Contributor

@bvrooman bvrooman Dec 2, 2022

Choose a reason for hiding this comment

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

Rather than a question of lifetimes, I think (if I'm understanding Brandon's original comment correctly) it's more of a question about when to use references vs. when to use copies. Personally, I think IDs (similar to ints) are something you should be able to copy without much fear of overhead. But I can also understand that in a performance-critical application, avoiding copies is a good general goal. (But there is the potential cost of accumulating cache misses if this is in any sort of loop!)

@xgreenx xgreenx force-pushed the feature/move-tabels-to-interfaces branch from c69eee9 to d2a9dd0 Compare November 30, 2022 23:43
Base automatically changed from feature/move-tabels-to-interfaces to master December 1, 2022 00:45
@xgreenx xgreenx force-pushed the feature/update-state-and-balances-root branch from bac9d86 to 17b3214 Compare December 2, 2022 02:02
bvrooman
bvrooman previously approved these changes Dec 2, 2022
Comment on lines 917 to 926
let expected_utxo_id = |contract: &ContractRef<&mut Database>| {
let maybe_utxo_id = contract.utxo()?.map(|utxo| utxo.into_owned());
let expected_utxo_id = if self.config.utxo_validation {
maybe_utxo_id
.ok_or(Error::ContractUtxoMissing(*contract_id))?
.into_owned()
.ok_or_else(|| Error::ContractUtxoMissing(*contract.contract_id()))?
} else {
maybe_utxo_id.unwrap_or_default().into_owned()
maybe_utxo_id.unwrap_or_default()
};
Result::<_, Error>::Ok(expected_utxo_id)
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of a lambda, can expected_utxo_id be replaced by a getter on the ContractRef interface? If that is practical, I think it would make the readability a bit better and consistent:

let mut contract = ContractRef::new(&mut *db, contract_id);
*utxo_id = contract.utxo_id()?;
*balance_root = contract.balance_root()?;
*state_root = contract.state_root()?;

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 tried to avoid any business logic inside of those wrappers=)

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point. I am not strongly opinionated either way. As usual with software, there are pros and cons to either approach!

@@ -2449,6 +2574,281 @@ mod tests {
assert_eq!(coin.status, CoinStatus::Spent);
}

#[test]
Copy link
Contributor

Choose a reason for hiding this comment

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

Just an aside comment: now that we have so many lines of tests in this file, it may be worth extracting the test suite to its own file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We will, but in a separate PR. executor.rs should live in the fuel-executor, and during this refactoring, we will definitely split the logic across several files and the same with tests.

@bvrooman bvrooman requested a review from a team December 2, 2022 16:40
bvrooman
bvrooman previously approved these changes Dec 2, 2022
@bvrooman bvrooman requested a review from a team December 2, 2022 20:03
@xgreenx xgreenx requested a review from Voxelot December 3, 2022 15:38
Comment on lines 945 to 946
// Needed to convince the compiler that tx is taken by ref here
#[allow(clippy::needless_borrow)]
Copy link
Member

Choose a reason for hiding this comment

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

Does this clippy annotation still apply?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, we can remove

*header.height(),
&tx_id,
tx_db_transaction.deref_mut(),
vm_result.tx().inputs(),
vm_result.tx().outputs(),
)?;
self.compute_outputs(
Copy link
Member

@Voxelot Voxelot Dec 6, 2022

Choose a reason for hiding this comment

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

It looks like we're persisting the outputs before we compute their state/balance roots etc. It seems like this should've been caught in tests if we're checking that the balance / state roots on contract outputs are set and persisted correctly 🤔 .

Copy link
Collaborator Author

@xgreenx xgreenx Dec 6, 2022

Choose a reason for hiding this comment

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

We have a test but without storage check=( persist_output_utxos persist only outputs related to utxo=) It is the expected ordering of methods. Added a comment to make it clear

} = executor.execute(ExecutionBlock::Production(block)).unwrap();

let empty_state = Bytes32::from(*empty_sum());
let executed_tx = block.transactions()[2].as_script().unwrap();
Copy link
Member

@Voxelot Voxelot Dec 6, 2022

Choose a reason for hiding this comment

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

we should also be testing:

  1. that the tx in the db matches the tx in the block
  2. that utxos in the db match the expected state, since those are distinct from the output types on the tx

related: https://github.com/FuelLabs/fuel-core/pull/789/files#r1040374277

Voxelot
Voxelot previously requested changes Dec 6, 2022
Copy link
Member

@Voxelot Voxelot left a comment

Choose a reason for hiding this comment

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

blocking the previous approval due to bug in utxo persistence

Added check of the storage into unit test.
Added comment to highlight the ordering of operations.
@Voxelot Voxelot dismissed their stale review December 6, 2022 03:30

not a bug

@Voxelot
Copy link
Member

Voxelot commented Dec 6, 2022

Ahh that's right, these output calculations aren't persisted to the utxo state set. They are just metadata on the tx itself. Thanks for clarifying.

@@ -50,6 +51,14 @@ pub struct ExecutionResult {
/// The list of skipped transactions with corresponding errors. Those transactions were
/// not included in the block and didn't affect the state of the blockchain.
pub skipped_transactions: Vec<(Transaction, Error)>,
/// The status of the transactions execution included into the block.
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] I think this sentence might need to be adjusted along the lines of "The status of executed transactions included in the block"

Copy link
Contributor

@ControlCplusControlV ControlCplusControlV left a comment

Choose a reason for hiding this comment

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

LGTM

@xgreenx xgreenx merged commit bad93d1 into master Dec 6, 2022
@xgreenx xgreenx deleted the feature/update-state-and-balances-root branch December 6, 2022 11:57
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.

4 participants