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

fix(genesis): set salt of contract on execution of genesis state configuration #2322

Merged
merged 13 commits into from
Oct 11, 2024
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## Fixed
### Fixed
- [2320](https://github.com/FuelLabs/fuel-core/issues/2320): Prevent `/health` and `/v1/health` from being throttled by the concurrency limiter.
- [2322](https://github.com/FuelLabs/fuel-core/issues/2322): Set the salt of genesis contracts to zero on execution.

## [Version 0.38.0]

Expand Down
11 changes: 11 additions & 0 deletions crates/client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,17 @@ impl FuelClient {
Ok(messages)
}

pub async fn contract_info(
&self,
contract: &ContractId,
) -> io::Result<Option<types::Contract>> {
let query = schema::contract::ContractByIdQuery::build(ContractByIdArgs {
id: (*contract).into(),
});
let contract_info = self.query(query).await?.contract.map(Into::into);
Ok(contract_info)
}

pub async fn message_status(&self, nonce: &Nonce) -> io::Result<MessageStatus> {
let query = schema::message::MessageStatusQuery::build(MessageStatusArgs {
nonce: (*nonce).into(),
Expand Down
18 changes: 2 additions & 16 deletions crates/fuel-core/src/combined_database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,15 +222,6 @@ impl CombinedDatabase {
Ok(state_config)
}

/// Converts the combined database into a genesis combined database.
pub fn into_genesis(self) -> CombinedGenesisDatabase {
CombinedGenesisDatabase {
on_chain: self.on_chain.into_genesis(),
off_chain: self.off_chain.into_genesis(),
relayer: self.relayer.into_genesis(),
}
}

/// Rollbacks the state of the blockchain to a specific block height.
pub fn rollback_to<S>(
&self,
Expand Down Expand Up @@ -367,9 +358,8 @@ pub trait ShutdownListener {
/// genesis databases into one entity.
#[derive(Default, Clone)]
pub struct CombinedGenesisDatabase {
on_chain: GenesisDatabase<OnChain>,
off_chain: GenesisDatabase<OffChain>,
relayer: GenesisDatabase<Relayer>,
pub on_chain: GenesisDatabase<OnChain>,
pub off_chain: GenesisDatabase<OffChain>,
}

impl CombinedGenesisDatabase {
Expand All @@ -380,8 +370,4 @@ impl CombinedGenesisDatabase {
pub fn off_chain(&self) -> &GenesisDatabase<OffChain> {
&self.off_chain
}

pub fn relayer(&self) -> &GenesisDatabase<Relayer> {
&self.relayer
}
}
27 changes: 18 additions & 9 deletions crates/fuel-core/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,24 @@ where
Ok(Self::new(Arc::new(db)))
}

/// Converts to an unchecked database.
/// Panics if the height is already set.
pub fn into_genesis(self) -> GenesisDatabase<Description> {
assert!(
!self.stage.height.lock().is_some(),
"Height is already set for `{}`",
Description::name()
);
GenesisDatabase::new(self.into_inner().data)
/// Converts the regular database to an unchecked database.
///
/// Returns an error in the case regular database is initialized with the `GenesisDatabase`,
/// to highlight that it is a bad idea and it is unsafe.
pub fn into_genesis(
self,
) -> core::result::Result<GenesisDatabase<Description>, GenesisDatabase<Description>>
{
if !self.stage.height.lock().is_some() {
Ok(GenesisDatabase::new(self.into_inner().data))
} else {
tracing::warn!(
"Converting regular database into genesis, \
while height is already set for `{}`",
Description::name()
);
Err(GenesisDatabase::new(self.into_inner().data))
}
}
}

Expand Down
22 changes: 21 additions & 1 deletion crates/fuel-core/src/graphql_api/storage/contracts.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
use fuel_core_chain_config::{
AsTable,
StateConfig,
TableEntry,
};
use fuel_core_storage::{
blueprint::plain::Plain,
codec::{
Expand All @@ -9,7 +14,10 @@ use fuel_core_storage::{
};
use fuel_core_types::{
entities::contract::ContractsInfoType,
fuel_tx::ContractId,
fuel_tx::{
ContractId,
Salt,
},
};

/// Contract info
Expand All @@ -31,6 +39,18 @@ impl TableWithBlueprint for ContractsInfo {
}
}

impl AsTable<ContractsInfo> for StateConfig {
fn as_table(&self) -> Vec<TableEntry<ContractsInfo>> {
self.contracts
.iter()
.map(|contracts_config| TableEntry {
key: contracts_config.contract_id,
value: ContractsInfoType::V1(Salt::zeroed().into()),
})
.collect()
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
8 changes: 8 additions & 0 deletions crates/fuel-core/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,14 @@ impl FuelService {
}
}

// repopulate missing tables
genesis::recover_missing_tables_from_genesis_state_config(
watcher.clone(),
&self.shared.config,
&self.shared.database,
)
.await?;

self.override_chain_config_if_needed()
}
}
Expand Down
75 changes: 63 additions & 12 deletions crates/fuel-core/src/service/genesis.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
use self::importer::SnapshotImporter;
use crate::{
combined_database::CombinedDatabase,
combined_database::{
CombinedDatabase,
CombinedGenesisDatabase,
},
database::{
database_description::{
off_chain::OffChain,
on_chain::OnChain,
},
genesis_progress::GenesisMetadata,
Database,
},
service::config::Config,
};
use fuel_core_chain_config::GenesisCommitment;
use fuel_core_services::StateWatcher;
use fuel_core_storage::{
iter::IteratorOverTable,
not_found,
tables::{
ConsensusParametersVersions,
StateTransitionBytecodeVersions,
UploadedBytecodes,
},
transactional::{
AtomicView,
Changes,
IntoTransaction,
ReadTransaction,
Expand Down Expand Up @@ -53,16 +60,14 @@ use fuel_core_types::{
};
use itertools::Itertools;

pub use exporter::Exporter;
pub use task_manager::NotifyCancel;

mod exporter;
mod importer;
mod progress;
mod task_manager;

pub use exporter::Exporter;
pub use task_manager::NotifyCancel;

use self::importer::SnapshotImporter;

/// Performs the importing of the genesis block from the snapshot.
pub async fn execute_genesis_block(
watcher: StateWatcher,
Expand All @@ -71,10 +76,24 @@ pub async fn execute_genesis_block(
) -> anyhow::Result<UncommittedImportResult<Changes>> {
let genesis_block = create_genesis_block(config);
tracing::info!("Genesis block created: {:?}", genesis_block.header());
let db = db.clone().into_genesis();
let on_chain = db
.on_chain()
.clone()
.into_genesis()
.map_err(|_| anyhow::anyhow!("On chain database is already initialized"))?;
let off_chain = db
.off_chain()
.clone()
.into_genesis()
.map_err(|_| anyhow::anyhow!("Off chain database is already initialized"))?;

let genesis_db = CombinedGenesisDatabase {
on_chain,
off_chain,
};

SnapshotImporter::import(
db.clone(),
genesis_db.clone(),
genesis_block.clone(),
config.snapshot_reader.clone(),
watcher,
Expand All @@ -93,10 +112,10 @@ pub async fn execute_genesis_block(
let chain_config = config.snapshot_reader.chain_config();
let genesis = Genesis {
chain_config_hash: chain_config.root()?.into(),
coins_root: db.on_chain().genesis_coins_root()?.into(),
messages_root: db.on_chain().genesis_messages_root()?.into(),
contracts_root: db.on_chain().genesis_contracts_root()?.into(),
transactions_root: db.on_chain().processed_transactions_root()?.into(),
coins_root: genesis_db.on_chain().genesis_coins_root()?.into(),
messages_root: genesis_db.on_chain().genesis_messages_root()?.into(),
contracts_root: genesis_db.on_chain().genesis_contracts_root()?.into(),
transactions_root: genesis_db.on_chain().processed_transactions_root()?.into(),
};

let consensus = Consensus::Genesis(genesis);
Expand Down Expand Up @@ -150,6 +169,38 @@ pub async fn execute_genesis_block(
Ok(result)
}

pub async fn recover_missing_tables_from_genesis_state_config(
watcher: StateWatcher,
config: &Config,
db: &CombinedDatabase,
) -> anyhow::Result<()> {
// TODO: The code below only modifies the contract of the genesis block of the off chain database.
// It is safe to initialize some missing data, for now, but it can change in the future.
// We plan to remove this code later, see: https://github.com/FuelLabs/fuel-core/issues/2326
let Err(off_chain) = db.off_chain().clone().into_genesis() else {
return Ok(())
};

let genesis_db = CombinedGenesisDatabase {
on_chain: Database::default(),
off_chain,
};
let genesis_block = db
.on_chain()
.latest_view()?
.genesis_block()?
.ok_or(not_found!("Genesis block"))?;
let genesis_block = genesis_block.uncompress(vec![]);

SnapshotImporter::repopulate_maybe_missing_tables(
xgreenx marked this conversation as resolved.
Show resolved Hide resolved
genesis_db,
genesis_block,
config.snapshot_reader.clone(),
watcher,
)
.await
}

#[cfg(feature = "test-helpers")]
pub async fn execute_and_commit_genesis_block(
config: &Config,
Expand Down
16 changes: 16 additions & 0 deletions crates/fuel-core/src/service/genesis/importer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ impl SnapshotImporter {
.await
}

pub async fn repopulate_maybe_missing_tables(
db: CombinedGenesisDatabase,
genesis_block: Block,
snapshot_reader: SnapshotReader,
watcher: StateWatcher,
) -> anyhow::Result<()> {
let mut importer = Self::new(db, genesis_block, snapshot_reader, watcher);

// the below tables were not populated from the genesis snapshot on older versions
importer.spawn_worker_off_chain::<ContractsInfo, ContractsInfo>()?;
rymnc marked this conversation as resolved.
Show resolved Hide resolved

importer.task_manager.wait().await?;
Ok(())
}

async fn run_workers(mut self) -> anyhow::Result<()> {
tracing::info!("Running imports");
self.spawn_worker_on_chain::<Coins>()?;
Expand All @@ -130,6 +145,7 @@ impl SnapshotImporter {
self.spawn_worker_off_chain::<FuelBlocks, OldFuelBlocks>()?;
self.spawn_worker_off_chain::<Transactions, OldTransactions>()?;
self.spawn_worker_off_chain::<SealedBlockConsensus, OldFuelBlockConsensus>()?;
self.spawn_worker_off_chain::<ContractsInfo, ContractsInfo>()?;
self.spawn_worker_off_chain::<Transactions, ContractsInfo>()?;
self.spawn_worker_off_chain::<OldTransactions, ContractsInfo>()?;
self.spawn_worker_off_chain::<OldFuelBlocks, OldFuelBlocks>()?;
Expand Down
21 changes: 19 additions & 2 deletions crates/fuel-core/src/service/genesis/importer/off_chain.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::borrow::Cow;

use crate::{
database::{
database_description::off_chain::OffChain,
Expand Down Expand Up @@ -38,6 +36,7 @@ use fuel_core_storage::{
StorageAsMut,
};
use fuel_core_types::services::executor::Event;
use std::borrow::Cow;

use super::{
import_task::ImportTable,
Expand Down Expand Up @@ -134,6 +133,24 @@ impl ImportTable for Handler<OwnedCoins, Coins> {
}
}

impl ImportTable for Handler<ContractsInfo, ContractsInfo> {
type TableInSnapshot = ContractsInfo;
type TableBeingWritten = ContractsInfo;
type DbDesc = OffChain;

fn process(
&mut self,
group: Vec<TableEntry<Self::TableInSnapshot>>,
tx: &mut StorageTransaction<&mut GenesisDatabase<Self::DbDesc>>,
) -> anyhow::Result<()> {
for entry in group {
tx.storage::<ContractsInfo>()
.insert(&entry.key, &entry.value)?;
}
Ok(())
}
}

impl ImportTable for Handler<ContractsInfo, Transactions> {
type TableInSnapshot = Transactions;
type TableBeingWritten = ContractsInfo;
Expand Down
24 changes: 24 additions & 0 deletions tests/tests/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use rand::SeedableRng;

use fuel_core::chain_config::{
CoinConfig,
ContractConfig,
StateConfig,
};
use rstest::rstest;
Expand Down Expand Up @@ -414,3 +415,26 @@ async fn can_get_message_proof() {
assert_eq!(log[1].rb().unwrap(), 1);
assert_eq!(logd.data().unwrap(), db_data);
}

#[tokio::test]
async fn can_get_genesis_contract_salt() {
// given
let contract = ContractConfig::default();
let contract_id = contract.contract_id;
let service_config = Config::local_node_with_state_config(StateConfig {
contracts: vec![contract],
..Default::default()
});

// when
let node =
FuelService::from_database(Database::<OnChain>::in_memory(), service_config)
.await
.unwrap();
let client = FuelClient::from(node.bound_address);

// then
let ret = client.contract_info(&contract_id).await.unwrap().unwrap();

assert_eq!(ret.salt, Salt::zeroed());
}
Loading