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

Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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.

7 changes: 7 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,13 @@ num_enum = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }

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

[features]
default = ["std"]
std = ["fuel-core-storage/std", "fuel-core-types/std"]
Expand Down
293 changes: 282 additions & 11 deletions crates/fraud_proofs/global_merkle_root/storage/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,37 +64,39 @@ use fuel_core_types::{
};

pub trait UpdateMerkleizedTables {
fn update_merklized_tables(
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,275 @@ 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() {
netrome marked this conversation as resolved.
Show resolved Hide resolved
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 {
netrome marked this conversation as resolved.
Show resolved Hide resolved
utxo_id,
..Default::default()
});

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

let coin_was_inserted_before_process_input = storage_update_tx
.storage
.storage_as_ref::<Coins>()
.get(&utxo_id)
.unwrap()
.is_some();

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

storage_tx.commit().unwrap();

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

// Then
assert!(coin_was_inserted_before_process_input);
assert!(coin_doesnt_exist_after_process_input);
}

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