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

fault_proving(global_roots): Initial test suite for merklized storage updates #2598

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Added
- [2553](https://github.com/FuelLabs/fuel-core/pull/2553): Scaffold global merkle root storage crate.
- [2598](https://github.com/FuelLabs/fuel-core/pull/2598): Add initial test suite for global merkle root storage updates.

### Fixed
- [2632](https://github.com/FuelLabs/fuel-core/pull/2632): Improved performance of certain async trait impls in the gas price service.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/fraud_proofs/global_merkle_root/storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ num_enum = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }

[dev-dependencies]
fuel-core-storage = { workspace = true, features = ["alloc", "test-helpers"] }
rand = { workspace = true }

[features]
default = ["std"]
std = ["fuel-core-storage/std", "fuel-core-types/std"]
Expand Down
285 changes: 273 additions & 12 deletions crates/fraud_proofs/global_merkle_root/storage/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,38 +63,40 @@ use fuel_core_types::{
},
};

pub trait UpdateMerkleizedTables {
fn update_merklized_tables(
pub trait UpdateMerkleizedTables: Sized {
fn update_merkleized_tables(
&mut self,
chain_id: ChainId,
block: &Block,
) -> anyhow::Result<()>;
) -> anyhow::Result<&mut Self>;
}

impl<Storage> UpdateMerkleizedTables for StorageTransaction<Storage>
where
Storage: KeyValueInspect<Column = Column>,
{
fn update_merklized_tables(
fn update_merkleized_tables(
&mut self,
chain_id: ChainId,
block: &Block,
) -> anyhow::Result<()> {
let mut update_transaction = UpdateMerklizedTablesTransaction {
) -> anyhow::Result<&mut Self> {
let mut update_transaction = UpdateMerkleizedTablesTransaction {
chain_id,
storage: self,
};

update_transaction.process_block(block)
update_transaction.process_block(block)?;

Ok(self)
}
}

struct UpdateMerklizedTablesTransaction<'a, Storage> {
struct UpdateMerkleizedTablesTransaction<'a, Storage> {
chain_id: ChainId,
storage: &'a mut StorageTransaction<Storage>,
}

impl<'a, Storage> UpdateMerklizedTablesTransaction<'a, Storage>
impl<'a, Storage> UpdateMerkleizedTablesTransaction<'a, Storage>
where
Storage: KeyValueInspect<Column = Column>,
{
Expand Down Expand Up @@ -305,6 +307,265 @@ impl TransactionOutputs for Transaction {
}
}

// TODO(#2582): Add tests (https://github.com/FuelLabs/fuel-core/issues/2582)
#[test]
fn dummy() {}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;

use fuel_core_storage::{
structured_storage::test::InMemoryStorage,
transactional::{
ReadTransaction,
WriteTransaction,
},
StorageAsRef,
};
use fuel_core_types::fuel_tx::{
Bytes32,
ContractId,
TxId,
};

use rand::{
rngs::StdRng,
Rng,
SeedableRng,
};

#[test]
/// When encountering a transaction with a coin output,
/// `process_output` should ensure this coin is
/// populated in the `Coins` table.
fn process_output__should_insert_coin() {
let mut rng = StdRng::seed_from_u64(1337);

// Given
let mut storage: InMemoryStorage<Column> = InMemoryStorage::default();
let mut storage_tx = storage.write_transaction();
let mut storage_update_tx =
storage_tx.construct_update_merkleized_tables_transaction();

let tx_pointer = random_tx_pointer(&mut rng);
let utxo_id = random_utxo_id(&mut rng);
let inputs = vec![];

let output_amount = rng.gen();
let output_address = random_address(&mut rng);
let output = Output::Coin {
to: output_address,
amount: output_amount,
asset_id: AssetId::zeroed(),
};

// When
storage_update_tx
.process_output(tx_pointer, utxo_id, &inputs, &output)
.unwrap();

storage_tx.commit().unwrap();

let inserted_coin = storage
.read_transaction()
.storage_as_ref::<Coins>()
.get(&utxo_id)
.unwrap()
.unwrap()
.into_owned();

// Then
assert_eq!(*inserted_coin.amount(), output_amount);
assert_eq!(*inserted_coin.owner(), output_address);
}

#[test]
/// When encountering a transaction with a contract created output,
/// `process_output` should ensure an appropriate contract UTxO is
/// populated in the `ContractCreated` table.
fn process_output__should_insert_latest_contract_utxo_when_contract_created() {
let mut rng = StdRng::seed_from_u64(1337);

// Given
let mut storage: InMemoryStorage<Column> = InMemoryStorage::default();
let mut storage_tx = storage.write_transaction();
let mut storage_update_tx =
storage_tx.construct_update_merkleized_tables_transaction();

let tx_pointer = random_tx_pointer(&mut rng);
let utxo_id = random_utxo_id(&mut rng);
let inputs = vec![];

let contract_id = random_contract_id(&mut rng);
let output = Output::ContractCreated {
contract_id,
state_root: Bytes32::zeroed(),
};

// When
storage_update_tx
.process_output(tx_pointer, utxo_id, &inputs, &output)
.unwrap();

storage_tx.commit().unwrap();

let inserted_contract_utxo = storage
.read_transaction()
.storage_as_ref::<ContractsLatestUtxo>()
.get(&contract_id)
.unwrap()
.unwrap()
.into_owned();

// Then
assert_eq!(inserted_contract_utxo.utxo_id(), &utxo_id);
}

#[test]
/// When encountering a transaction with a contract output,
/// `process_output` should ensure an appropriate contract UTxO is
/// populated in the `ContractCreated` table.
fn process_output__should_update_latest_contract_utxo_when_interacting_with_contract()
{
let mut rng = StdRng::seed_from_u64(1337);

// Given
let mut storage: InMemoryStorage<Column> = InMemoryStorage::default();
let mut storage_tx = storage.write_transaction();
let mut storage_update_tx =
storage_tx.construct_update_merkleized_tables_transaction();

let tx_pointer = random_tx_pointer(&mut rng);
let utxo_id = random_utxo_id(&mut rng);

let contract_id = random_contract_id(&mut rng);
let input_contract = input::contract::Contract {
contract_id,
..Default::default()
};
let inputs = vec![Input::Contract(input_contract)];

let output_contract = output::contract::Contract {
input_index: 0,
..Default::default()
};

let output = Output::Contract(output_contract);

// When
storage_update_tx
.process_output(tx_pointer, utxo_id, &inputs, &output)
.unwrap();

storage_tx.commit().unwrap();

let inserted_contract_utxo = storage
.read_transaction()
.storage_as_ref::<ContractsLatestUtxo>()
.get(&contract_id)
.unwrap()
.unwrap()
.into_owned();

// Then
assert_eq!(inserted_contract_utxo.utxo_id(), &utxo_id);
}

#[test]
/// When encountering a transaction with a coin input,
/// `process_input` should ensure this coin is
/// removed from the `Coins` table, as this coin is no longer
/// a part of the active UTxO set.
fn process_input__should_remove_coin() {
Copy link
Member

Choose a reason for hiding this comment

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

As someone with little context here, it's not obvious to me what the difference between process_output__should_insert_coin and process_input__should_remove_coin are. It would be nice to explain the conditions for coin being added or removed in the test name so I know what I'm looking for.

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'll add some comments. Just restating here, the difference is when a transaction produces a coin as an output, it should be added to the Coins table, since we have created a coin. When a coin is spent as an input, we remove it because it's no longer in the active UTXO set. I'll figure out a nice way to explain this in the test. Thanks for pointing this out. I'd love for the code to be as understandable as possible with as little context as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 92b1d22

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me know what you think.

let mut rng = StdRng::seed_from_u64(1337);

// Given
let mut storage: InMemoryStorage<Column> = InMemoryStorage::default();
let mut storage_tx = storage.write_transaction();
let mut storage_update_tx =
storage_tx.construct_update_merkleized_tables_transaction();

let output_amount = rng.gen();
let output_address = random_address(&mut rng);
let tx_pointer = random_tx_pointer(&mut rng);
let utxo_id = random_utxo_id(&mut rng);
let inputs = vec![];

let output = Output::Coin {
to: output_address,
amount: output_amount,
asset_id: AssetId::zeroed(),
};

let input = Input::CoinSigned(CoinSigned {
utxo_id,
..Default::default()
});

// When
storage_update_tx
.process_output(tx_pointer, utxo_id, &inputs, &output)
.unwrap();

storage_update_tx.process_input(&input).unwrap();

storage_tx.commit().unwrap();

// Then
assert!(storage
.read_transaction()
.storage_as_ref::<Coins>()
.get(&utxo_id)
.unwrap()
.is_none());
}

fn random_utxo_id(rng: &mut impl rand::RngCore) -> UtxoId {
let mut txid = TxId::default();
rng.fill_bytes(txid.as_mut());
let output_index = rng.gen();

UtxoId::new(txid, output_index)
}

fn random_tx_pointer(rng: &mut impl rand::RngCore) -> TxPointer {
let block_height = BlockHeight::new(rng.gen());
let tx_index = rng.gen();

TxPointer::new(block_height, tx_index)
}

fn random_address(rng: &mut impl rand::RngCore) -> Address {
let mut address = Address::default();
rng.fill_bytes(address.as_mut());

address
}

fn random_contract_id(rng: &mut impl rand::RngCore) -> ContractId {
let mut contract_id = ContractId::default();
rng.fill_bytes(contract_id.as_mut());

contract_id
}

trait ConstructUpdateMerkleizedTablesTransactionForTests<'a>: Sized + 'a {
Copy link
Contributor

Choose a reason for hiding this comment

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

would it not be better to leverage the BuilderPattern in this case?

E.g. something like

UpdateMerkleizedTablesTransactionBuilder::new().with_storage(storage).with_chain_id(&chain_id)

But probably best to do it in a follow-up and merge this one already :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes and yes :) But again this is only for tests, so I'm not sure if it's worth it. I'd happily approve a PR if you implement it as a follow-up.

type Storage;
fn construct_update_merkleized_tables_transaction(
self,
) -> UpdateMerkleizedTablesTransaction<'a, Self::Storage>;
}

impl<'a, Storage> ConstructUpdateMerkleizedTablesTransactionForTests<'a>
for &'a mut StorageTransaction<Storage>
{
type Storage = Storage;

fn construct_update_merkleized_tables_transaction(
self,
) -> UpdateMerkleizedTablesTransaction<'a, Self::Storage> {
UpdateMerkleizedTablesTransaction {
chain_id: ChainId::default(),
storage: self,
}
}
}
}
Loading