From 2eaa6d4260f6c0971623143ee63fa333753b77a7 Mon Sep 17 00:00:00 2001 From: Brandon Vrooman Date: Fri, 26 Jan 2024 13:23:06 -0500 Subject: [PATCH] feat: Versionable CompressedCoin (#1628) Related issues: - https://github.com/FuelLabs/fuel-core/issues/1552 --- CHANGELOG.md | 9 +- crates/chain-config/src/config/coin.rs | 12 +- crates/fuel-core/src/coins_query.rs | 11 +- crates/fuel-core/src/database/coin.rs | 16 +-- crates/fuel-core/src/executor.rs | 117 +++++++------------ crates/fuel-core/src/service/genesis.rs | 22 +++- crates/services/executor/src/executor.rs | 24 ++-- crates/services/txpool/src/test_helpers.rs | 11 +- crates/types/src/entities/coins/coin.rs | 128 ++++++++++++++++++--- 9 files changed, 209 insertions(+), 141 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 875321c4918..04e2e8ffc4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,10 +21,11 @@ Description of the upcoming release here. - [#1601](https://github.com/FuelLabs/fuel-core/pull/1601): Fix formatting in docs and check that `cargo doc` passes in the CI. #### Breaking -- [#1616](https://github.com/FuelLabs/fuel-core/pull/1616) Make `BlockHeader` type a version-able enum -- [#1614](https://github.com/FuelLabs/fuel-core/pull/#1614): Use the default consensus key regardless of trigger mode. The change is breaking because it removes the `--dev-keys` argument. If the `debug` flag is set, the default consensus key will be used, regardless of the trigger mode. -- [#1596](https://github.com/FuelLabs/fuel-core/pull/1596) Make `Consensus` type a version-able enum -- [#1593](https://github.com/FuelLabs/fuel-core/pull/1593) Make `Block` type a version-able enum +- [#1628](https://github.com/FuelLabs/fuel-core/pull/1628): Make `CompressedCoin` type a version-able enum +- [#1616](https://github.com/FuelLabs/fuel-core/pull/1616): Make `BlockHeader` type a version-able enum +- [#1614](https://github.com/FuelLabs/fuel-core/pull/1614): Use the default consensus key regardless of trigger mode. The change is breaking because it removes the `--dev-keys` argument. If the `debug` flag is set, the default consensus key will be used, regardless of the trigger mode. +- [#1596](https://github.com/FuelLabs/fuel-core/pull/1596): Make `Consensus` type a version-able enum +- [#1593](https://github.com/FuelLabs/fuel-core/pull/1593): Make `Block` type a version-able enum - [#1576](https://github.com/FuelLabs/fuel-core/pull/1576): The change moves the implementation of the storage traits for required tables from `fuel-core` to `fuel-core-storage` crate. The change also adds a more flexible configuration of the encoding/decoding per the table and allows the implementation of specific behaviors for the table in a much easier way. It unifies the encoding between database, SMTs, and iteration, preventing mismatching bytes representation on the Rust type system level. Plus, it increases the re-usage of the code by applying the same blueprint to other tables. It is a breaking PR because it changes database encoding/decoding for some tables. diff --git a/crates/chain-config/src/config/coin.rs b/crates/chain-config/src/config/coin.rs index b0e1c6efa32..5ad447f58ed 100644 --- a/crates/chain-config/src/config/coin.rs +++ b/crates/chain-config/src/config/coin.rs @@ -58,13 +58,11 @@ pub struct CoinConfig { impl GenesisCommitment for CompressedCoin { fn root(&self) -> anyhow::Result { - let Self { - owner, - amount, - asset_id, - maturity, - tx_pointer, - } = self; + let owner = self.owner(); + let amount = self.amount(); + let asset_id = self.asset_id(); + let maturity = self.maturity(); + let tx_pointer = self.tx_pointer(); let coin_hash = *Hasher::default() .chain(owner) diff --git a/crates/fuel-core/src/coins_query.rs b/crates/fuel-core/src/coins_query.rs index 9c41fd06054..9cb6f24e938 100644 --- a/crates/fuel-core/src/coins_query.rs +++ b/crates/fuel-core/src/coins_query.rs @@ -950,13 +950,10 @@ mod tests { self.last_coin_index += 1; let id = UtxoId::new(Bytes32::from([0u8; 32]), index.try_into().unwrap()); - let coin = CompressedCoin { - owner, - amount, - asset_id, - maturity: Default::default(), - tx_pointer: Default::default(), - }; + let mut coin = CompressedCoin::default(); + coin.set_owner(owner); + coin.set_amount(amount); + coin.set_asset_id(asset_id); let db = &mut self.database; StorageMutate::::insert(db, &id, &coin).unwrap(); diff --git a/crates/fuel-core/src/database/coin.rs b/crates/fuel-core/src/database/coin.rs index 52a1a7e4a92..04b262592e4 100644 --- a/crates/fuel-core/src/database/coin.rs +++ b/crates/fuel-core/src/database/coin.rs @@ -78,7 +78,7 @@ impl StorageMutate for Database { key: &UtxoId, value: &CompressedCoin, ) -> Result, Self::Error> { - let coin_by_owner = owner_coin_id_key(&value.owner, key); + let coin_by_owner = owner_coin_id_key(value.owner(), key); // insert primary record let insert = self.data.storage_as_mut::().insert(key, value)?; // insert secondary index by owner @@ -92,7 +92,7 @@ impl StorageMutate for Database { // cleanup secondary index if let Some(coin) = &coin { - let key = owner_coin_id_key(&coin.owner, key); + let key = owner_coin_id_key(coin.owner(), key); self.storage_as_mut::().remove(&key)?; } @@ -142,12 +142,12 @@ impl Database { Ok(CoinConfig { tx_id: Some(*utxo_id.tx_id()), output_index: Some(utxo_id.output_index()), - tx_pointer_block_height: Some(coin.tx_pointer.block_height()), - tx_pointer_tx_idx: Some(coin.tx_pointer.tx_index()), - maturity: Some(coin.maturity), - owner: coin.owner, - amount: coin.amount, - asset_id: coin.asset_id, + tx_pointer_block_height: Some(coin.tx_pointer().block_height()), + tx_pointer_tx_idx: Some(coin.tx_pointer().tx_index()), + maturity: Some(*coin.maturity()), + owner: *coin.owner(), + amount: *coin.amount(), + asset_id: *coin.asset_id(), }) }) .collect::>>()?; diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 8caa1fba087..f511adc7c19 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -1248,32 +1248,20 @@ mod tests { .clone(); let first_input = tx2.inputs()[0].clone(); + let mut first_coin = CompressedCoin::default(); + first_coin.set_owner(*first_input.input_owner().unwrap()); + first_coin.set_amount(100); let second_input = tx2.inputs()[1].clone(); + let mut second_coin = CompressedCoin::default(); + second_coin.set_owner(*second_input.input_owner().unwrap()); + second_coin.set_amount(100); let db = &mut Database::default(); // Insert both inputs db.storage::() - .insert( - &first_input.utxo_id().unwrap().clone(), - &CompressedCoin { - owner: *first_input.input_owner().unwrap(), - amount: 100, - asset_id: AssetId::default(), - maturity: Default::default(), - tx_pointer: Default::default(), - }, - ) + .insert(&first_input.utxo_id().unwrap().clone(), &first_coin) .unwrap(); db.storage::() - .insert( - &second_input.utxo_id().unwrap().clone(), - &CompressedCoin { - owner: *second_input.input_owner().unwrap(), - amount: 100, - asset_id: AssetId::default(), - maturity: Default::default(), - tx_pointer: Default::default(), - }, - ) + .insert(&second_input.utxo_id().unwrap().clone(), &second_coin) .unwrap(); let executor = create_executor( db.clone(), @@ -1342,20 +1330,14 @@ mod tests { .clone(); let input = tx.inputs()[0].clone(); + let mut coin = CompressedCoin::default(); + coin.set_owner(*input.input_owner().unwrap()); + coin.set_amount(AMOUNT - 1); let db = &mut Database::default(); // Inserting a coin with `AMOUNT - 1` should cause a mismatching error during production. db.storage::() - .insert( - &input.utxo_id().unwrap().clone(), - &CompressedCoin { - owner: *input.input_owner().unwrap(), - amount: AMOUNT - 1, - asset_id: AssetId::default(), - maturity: Default::default(), - tx_pointer: Default::default(), - }, - ) + .insert(&input.utxo_id().unwrap().clone(), &coin) .unwrap(); let executor = create_executor( db.clone(), @@ -1405,19 +1387,13 @@ mod tests { .clone(); let input = tx.inputs()[1].clone(); + let mut coin = CompressedCoin::default(); + coin.set_owner(*input.input_owner().unwrap()); + coin.set_amount(100); let db = &mut Database::default(); db.storage::() - .insert( - &input.utxo_id().unwrap().clone(), - &CompressedCoin { - owner: *input.input_owner().unwrap(), - amount: 100, - asset_id: AssetId::default(), - maturity: Default::default(), - tx_pointer: Default::default(), - }, - ) + .insert(&input.utxo_id().unwrap().clone(), &coin) .unwrap(); let executor = create_executor( db.clone(), @@ -1979,18 +1955,12 @@ mod tests { .. }) = tx.inputs()[0] { - db.storage::() - .insert( - &utxo_id, - &CompressedCoin { - owner, - amount, - asset_id, - maturity: Default::default(), - tx_pointer: TxPointer::new(starting_block, starting_block_tx_idx), - }, - ) - .unwrap(); + let mut coin = CompressedCoin::default(); + coin.set_owner(owner); + coin.set_amount(amount); + coin.set_asset_id(asset_id); + coin.set_tx_pointer(TxPointer::new(starting_block, starting_block_tx_idx)); + db.storage::().insert(&utxo_id, &coin).unwrap(); } let executor = create_executor( @@ -2219,7 +2189,7 @@ mod tests { let maybe_utxo = database.storage::().get(&id).unwrap(); assert!(maybe_utxo.is_some()); let utxo = maybe_utxo.unwrap(); - assert!(utxo.amount > 0) + assert!(*utxo.amount() > 0) } _ => (), } @@ -2397,10 +2367,10 @@ mod tests { .message_is_spent(&message_data.nonce) .unwrap()); assert_eq!( - block_db_transaction + *block_db_transaction .coin(&UtxoId::new(tx_id, 0)) .unwrap() - .amount, + .amount(), amount + amount ); } @@ -2460,10 +2430,10 @@ mod tests { .message_is_spent(&message_data.nonce) .unwrap()); assert_eq!( - block_db_transaction + *block_db_transaction .coin(&UtxoId::new(tx_id, 0)) .unwrap() - .amount, + .amount(), amount ); } @@ -2727,18 +2697,15 @@ mod tests { // setup db with coin to spend let database = &mut &mut Database::default(); let coin_input = &tx.inputs()[0]; + let mut coin = CompressedCoin::default(); + coin.set_owner(*coin_input.input_owner().unwrap()); + coin.set_amount(coin_input.amount().unwrap()); + coin.set_asset_id(*coin_input.asset_id(&base_asset_id).unwrap()); + coin.set_maturity(coin_input.maturity().unwrap()); + coin.set_tx_pointer(TxPointer::new(Default::default(), block_tx_idx)); database .storage::() - .insert( - coin_input.utxo_id().unwrap(), - &CompressedCoin { - owner: *coin_input.input_owner().unwrap(), - amount: coin_input.amount().unwrap(), - asset_id: *coin_input.asset_id(&base_asset_id).unwrap(), - maturity: coin_input.maturity().unwrap(), - tx_pointer: TxPointer::new(Default::default(), block_tx_idx), - }, - ) + .insert(coin_input.utxo_id().unwrap(), &coin) .unwrap(); // make executor with db @@ -2802,18 +2769,14 @@ mod tests { // setup db with coin to spend let database = &mut &mut Database::default(); let coin_input = &tx.inputs()[0]; + let mut coin = CompressedCoin::default(); + coin.set_owner(*coin_input.input_owner().unwrap()); + coin.set_amount(coin_input.amount().unwrap()); + coin.set_asset_id(*coin_input.asset_id(&base_asset_id).unwrap()); + coin.set_maturity(coin_input.maturity().unwrap()); database .storage::() - .insert( - coin_input.utxo_id().unwrap(), - &CompressedCoin { - owner: *coin_input.input_owner().unwrap(), - amount: coin_input.amount().unwrap(), - asset_id: *coin_input.asset_id(&base_asset_id).unwrap(), - maturity: coin_input.maturity().unwrap(), - tx_pointer: TxPointer::default(), - }, - ) + .insert(coin_input.utxo_id().unwrap(), &coin) .unwrap(); // make executor with db diff --git a/crates/fuel-core/src/service/genesis.rs b/crates/fuel-core/src/service/genesis.rs index 022a587e2da..13561b1de60 100644 --- a/crates/fuel-core/src/service/genesis.rs +++ b/crates/fuel-core/src/service/genesis.rs @@ -39,7 +39,10 @@ use fuel_core_types::{ SealedBlock, }, entities::{ - coins::coin::CompressedCoin, + coins::coin::{ + CompressedCoin, + CompressedCoinV1, + }, contract::ContractUtxoInfo, message::Message, }, @@ -181,7 +184,7 @@ fn init_coin_state( }), ); - let coin = CompressedCoin { + let compressed_coin: CompressedCoin = CompressedCoinV1 { owner: coin.owner, amount: coin.amount, asset_id: coin.asset_id, @@ -190,19 +193,26 @@ fn init_coin_state( coin.tx_pointer_block_height.unwrap_or_default(), coin.tx_pointer_tx_idx.unwrap_or_default(), ), - }; + } + .into(); // ensure coin can't point to blocks in the future - if coin.tx_pointer.block_height() > state.height.unwrap_or_default() { + if compressed_coin.tx_pointer().block_height() + > state.height.unwrap_or_default() + { return Err(anyhow!( "coin tx_pointer height cannot be greater than genesis block" )) } - if db.storage::().insert(&utxo_id, &coin)?.is_some() { + if db + .storage::() + .insert(&utxo_id, &compressed_coin)? + .is_some() + { return Err(anyhow!("Coin should not exist")) } - coins_tree.push(coin.root()?.as_slice()) + coins_tree.push(compressed_coin.root()?.as_slice()) } } } diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index a2041c56f4d..9b17dd7b70b 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -33,7 +33,10 @@ use fuel_core_types::{ primitives::DaBlockHeight, }, entities::{ - coins::coin::CompressedCoin, + coins::coin::{ + CompressedCoin, + CompressedCoinV1, + }, contract::ContractUtxoInfo, }, fuel_asm::{ @@ -1007,9 +1010,9 @@ where | Input::CoinPredicate(CoinPredicate { utxo_id, .. }) => { if let Some(coin) = db.storage::().get(utxo_id)? { let coin_mature_height = coin - .tx_pointer + .tx_pointer() .block_height() - .saturating_add(*coin.maturity) + .saturating_add(**coin.maturity()) .into(); if block_height < coin_mature_height { return Err(TransactionValidityError::CoinHasNotMatured( @@ -1192,7 +1195,7 @@ where db, *utxo_id, *owner, *amount, *asset_id, *maturity, options, )?; - *tx_pointer = coin.tx_pointer; + *tx_pointer = *coin.tx_pointer(); } Input::Contract(Contract { ref mut utxo_id, @@ -1240,7 +1243,7 @@ where db, *utxo_id, *owner, *amount, *asset_id, *maturity, options, )?; - if tx_pointer != &coin.tx_pointer { + if tx_pointer != coin.tx_pointer() { return Err(ExecutorError::InvalidTransactionOutcome { transaction_id: tx_id, }) @@ -1371,13 +1374,15 @@ where .map(Cow::into_owned) } else { // if utxo validation is disabled, just assign this new input to the original block - Ok(CompressedCoin { + let coin = CompressedCoinV1 { owner, amount, asset_id, maturity, tx_pointer: Default::default(), - }) + } + .into(); + Ok(coin) } } @@ -1508,13 +1513,14 @@ where // This is because variable or transfer outputs won't have any value // if there's a revert or panic and shouldn't be added to the utxo set. if *amount > Word::MIN { - let coin = CompressedCoin { + let coin = CompressedCoinV1 { owner: *to, amount: *amount, asset_id: *asset_id, maturity: 0u32.into(), tx_pointer: TxPointer::new(block_height, tx_idx), - }; + } + .into(); if db.storage::().insert(&utxo_id, &coin)?.is_some() { return Err(ExecutorError::OutputAlreadyExists) diff --git a/crates/services/txpool/src/test_helpers.rs b/crates/services/txpool/src/test_helpers.rs index 5586abee542..1f342032929 100644 --- a/crates/services/txpool/src/test_helpers.rs +++ b/crates/services/txpool/src/test_helpers.rs @@ -130,13 +130,10 @@ pub(crate) fn setup_coin(rng: &mut StdRng, mock_db: Option<&MockDb>) -> (Coin, I } pub(crate) fn add_coin_to_state(input: Input, mock_db: Option<&MockDb>) -> (Coin, Input) { - let coin = CompressedCoin { - owner: *input.input_owner().unwrap(), - amount: TEST_COIN_AMOUNT, - asset_id: *input.asset_id(&AssetId::BASE).unwrap(), - maturity: Default::default(), - tx_pointer: Default::default(), - }; + let mut coin = CompressedCoin::default(); + coin.set_owner(*input.input_owner().unwrap()); + coin.set_amount(TEST_COIN_AMOUNT); + coin.set_asset_id(*input.asset_id(&AssetId::BASE).unwrap()); let utxo_id = *input.utxo_id().unwrap(); if let Some(mock_db) = mock_db { mock_db diff --git a/crates/types/src/entities/coins/coin.rs b/crates/types/src/entities/coins/coin.rs index c22d8cd8e4f..9ee3f22c5ad 100644 --- a/crates/types/src/entities/coins/coin.rs +++ b/crates/types/src/entities/coins/coin.rs @@ -40,21 +40,37 @@ pub struct Coin { impl Coin { /// Compress the coin to minimize the serialized size. pub fn compress(self) -> CompressedCoin { - CompressedCoin { + CompressedCoin::V1(CompressedCoinV1 { owner: self.owner, amount: self.amount, asset_id: self.asset_id, maturity: self.maturity, tx_pointer: self.tx_pointer, - } + }) } } /// The compressed version of the `Coin` with minimum fields required for /// the proper work of the blockchain. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum CompressedCoin { + /// CompressedCoin Version 1 + V1(CompressedCoinV1), +} + +#[cfg(any(test, feature = "test-helpers"))] +impl Default for CompressedCoin { + fn default() -> Self { + Self::V1(Default::default()) + } +} + +/// CompressedCoin Version 1 +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct CompressedCoin { +pub struct CompressedCoinV1 { /// The address with permission to spend this coin pub owner: Address, /// Amount of coins @@ -68,23 +84,101 @@ pub struct CompressedCoin { pub tx_pointer: TxPointer, } +impl From for CompressedCoin { + fn from(value: CompressedCoinV1) -> Self { + Self::V1(value) + } +} + impl CompressedCoin { /// Uncompress the coin. pub fn uncompress(self, utxo_id: UtxoId) -> Coin { - Coin { - utxo_id, - owner: self.owner, - amount: self.amount, - asset_id: self.asset_id, - maturity: self.maturity, - tx_pointer: self.tx_pointer, + match self { + CompressedCoin::V1(coin) => Coin { + utxo_id, + owner: coin.owner, + amount: coin.amount, + asset_id: coin.asset_id, + maturity: coin.maturity, + tx_pointer: coin.tx_pointer, + }, + } + } + + /// Get the owner of the coin + pub fn owner(&self) -> &Address { + match self { + CompressedCoin::V1(coin) => &coin.owner, + } + } + + /// Set the owner of the coin + pub fn set_owner(&mut self, owner: Address) { + match self { + CompressedCoin::V1(coin) => coin.owner = owner, + } + } + + /// Get the amount of the coin + pub fn amount(&self) -> &Word { + match self { + CompressedCoin::V1(coin) => &coin.amount, + } + } + + /// Set the amount of the coin + pub fn set_amount(&mut self, amount: Word) { + match self { + CompressedCoin::V1(coin) => coin.amount = amount, + } + } + + /// Get the asset ID of the coin + pub fn asset_id(&self) -> &AssetId { + match self { + CompressedCoin::V1(coin) => &coin.asset_id, + } + } + + /// Set the asset ID of the coin + pub fn set_asset_id(&mut self, asset_id: AssetId) { + match self { + CompressedCoin::V1(coin) => coin.asset_id = asset_id, + } + } + + /// Get the maturity of the coin + pub fn maturity(&self) -> &BlockHeight { + match self { + CompressedCoin::V1(coin) => &coin.maturity, + } + } + + /// Set the maturity of the coin + pub fn set_maturity(&mut self, maturity: BlockHeight) { + match self { + CompressedCoin::V1(coin) => coin.maturity = maturity, + } + } + + /// Get the TX Pointer of the coin + pub fn tx_pointer(&self) -> &TxPointer { + match self { + CompressedCoin::V1(coin) => &coin.tx_pointer, + } + } + + /// Set the TX Pointer of the coin + pub fn set_tx_pointer(&mut self, tx_pointer: TxPointer) { + match self { + CompressedCoin::V1(coin) => coin.tx_pointer = tx_pointer, } } /// Verifies the integrity of the coin. /// /// Returns `None`, if the `input` is not a coin. - /// Otherwise returns the result of the field comparison. + /// Otherwise, returns the result of the field comparison. pub fn matches_input(&self, input: &Input) -> Option { match input { Input::CoinSigned(CoinSigned { @@ -98,11 +192,13 @@ impl CompressedCoin { amount, asset_id, .. - }) => Some( - owner == &self.owner - && amount == &self.amount - && asset_id == &self.asset_id, - ), + }) => match self { + CompressedCoin::V1(coin) => Some( + owner == &coin.owner + && amount == &coin.amount + && asset_id == &coin.asset_id, + ), + }, _ => None, } }