-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
base: master
Are you sure you want to change the base?
Changes from all commits
05442ce
a370f4f
b9bdd5e
7382104
92b1d22
5a7ccd1
06dd547
1fa8a92
80bd2b6
c79a153
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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>, | ||
{ | ||
|
@@ -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() { | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
But probably best to do it in a follow-up and merge this one already :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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
andprocess_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.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 92b1d22
There was a problem hiding this comment.
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.