feat: allow syncing op-mainnet with only state and without importing blocks/receipts (#10850)

Co-authored-by: Alexey Shekhirin <a.shekhirin@gmail.com>
This commit is contained in:
joshieDo
2024-09-18 18:10:38 +01:00
committed by GitHub
parent 80c1159cb8
commit c2019e35de
15 changed files with 387 additions and 86 deletions

5
Cargo.lock generated
View File

@ -8027,6 +8027,11 @@ dependencies = [
[[package]]
name = "reth-optimism-primitives"
version = "1.0.7"
dependencies = [
"alloy-primitives",
"reth-primitives",
"reth-primitives-traits",
]
[[package]]
name = "reth-optimism-rpc"

View File

@ -1,5 +1,25 @@
# Sync OP Mainnet
To sync OP mainnet, bedrock state needs to be imported as a starting point. There are currently two ways:
* Minimal bootstrap: only state snapshot at Bedrock block is imported without any OVM historical data.
* Full bootstrap: state, blocks and receipts are imported.
## Minimal bootstrap
**The state snapshot at Bedrock block is required.** It can be exported from [op-geth](https://github.com/testinprod-io/op-erigon/blob/pcw109550/bedrock-db-migration/bedrock-migration.md#export-state) (**.jsonl**) or downloaded directly from [here](https://mega.nz/file/GdZ1xbAT#a9cBv3AqzsTGXYgX7nZc_3fl--tcBmOAIwIA5ND6kwc).
```sh
$ op-reth init-state --without-ovm --chain optimism --datadir op-mainnet world_trie_state.jsonl
$ op-reth node --chain optimism --datadir op-mainnet --debug.tip 0x098f87b75c8b861c775984f9d5dbe7b70cbbbc30fc15adb03a5044de0144f2d0 # block #125200000
```
## Full bootstrap
### Import state
To sync OP mainnet, the Bedrock datadir needs to be imported to use as starting point.
Blocks lower than the OP mainnet Bedrock fork, are built on the OVM and cannot be executed on the EVM.
For this reason, the chain segment from genesis until Bedrock, must be manually imported to circumvent
@ -10,7 +30,7 @@ Importing OP mainnet Bedrock datadir requires exported data:
- Blocks [and receipts] below Bedrock
- State snapshot at first Bedrock block
## Manual Export Steps
### Manual Export Steps
The `op-geth` Bedrock datadir can be downloaded from <https://datadirs.optimism.io/mainnet-bedrock.tar.zst>.
@ -18,9 +38,9 @@ To export the OVM chain from `op-geth`, clone the `testinprod-io/op-geth` repo a
<https://github.com/testinprod-io/op-geth/pull/1>. Commands to export blocks, receipts and state dump can be
found in `op-geth/migrate.sh`.
## Manual Import Steps
### Manual Import Steps
### 1. Import Blocks
#### 1. Import Blocks
Imports a `.rlp` file of blocks.
@ -30,7 +50,7 @@ Import of >100 million OVM blocks, from genesis to Bedrock, completes in 45 minu
$ op-reth import-op <exported-blocks>
```
### 2. Import Receipts
#### 2. Import Receipts
This step is optional. To run a full node, skip this step. If however receipts are to be imported, the
corresponding transactions must already be imported (see [step 1](#1-import-blocks)).
@ -44,7 +64,7 @@ Import of >100 million OVM receipts, from genesis to Bedrock, completes in 30 mi
$ op-reth import-receipts-op <exported-receipts>
```
### 3. Import State
#### 3. Import State
Imports a `.jsonl` state dump. The block at which the state dump is made, must be the latest block in
reth's database. This should be block 105 235 063, the first Bedrock block (see [step 1](#1-import-blocks)).

View File

@ -17,7 +17,7 @@ use tracing::info;
#[derive(Debug, Parser)]
pub struct InitStateCommand<C: ChainSpecParser> {
#[command(flatten)]
env: EnvironmentArgs<C>,
pub env: EnvironmentArgs<C>,
/// JSONL file with state dump.
///
@ -37,7 +37,7 @@ pub struct InitStateCommand<C: ChainSpecParser> {
/// Allows init at a non-genesis block. Caution! Blocks must be manually imported up until
/// and including the non-genesis block to init chain at. See 'import' command.
#[arg(value_name = "STATE_DUMP_FILE", verbatim_doc_comment)]
state: PathBuf,
pub state: PathBuf,
}
impl<C: ChainSpecParser<ChainSpec = ChainSpec>> InitStateCommand<C> {
@ -71,5 +71,9 @@ pub fn init_at_state<N: NodeTypesWithDB<ChainSpec = ChainSpec>>(
let file = File::open(state_dump_path)?;
let reader = BufReader::new(file);
init_from_state_dump(reader, factory, etl_config)
let provider_rw = factory.provider_rw()?;
let hash = init_from_state_dump(reader, &provider_rw.0, etl_config)?;
provider_rw.commit()?;
Ok(hash)
}

View File

@ -16,6 +16,7 @@ reth-cli-commands.workspace = true
reth-consensus.workspace = true
reth-db = { workspace = true, features = ["mdbx"] }
reth-db-api.workspace = true
reth-db-common.workspace = true
reth-downloaders.workspace = true
reth-provider.workspace = true
reth-prune.workspace = true

View File

@ -12,7 +12,7 @@ use reth_downloaders::file_client::{
};
use reth_node_builder::NodeTypesWithEngine;
use reth_node_core::version::SHORT_VERSION;
use reth_optimism_primitives::bedrock_import::is_dup_tx;
use reth_optimism_primitives::bedrock::is_dup_tx;
use reth_provider::StageCheckpointReader;
use reth_prune::PruneModes;
use reth_stages::StageId;

View File

@ -15,7 +15,7 @@ use reth_downloaders::{
use reth_execution_types::ExecutionOutcome;
use reth_node_builder::{NodeTypesWithDB, NodeTypesWithEngine};
use reth_node_core::version::SHORT_VERSION;
use reth_optimism_primitives::bedrock_import::is_dup_tx;
use reth_optimism_primitives::bedrock::is_dup_tx;
use reth_primitives::Receipts;
use reth_provider::{
writer::UnifiedStorageWriter, DatabaseProviderFactory, OriginalValuesKnown, ProviderFactory,

View File

@ -0,0 +1,136 @@
use alloy_primitives::B256;
use reth_db::Database;
use reth_optimism_primitives::bedrock::{BEDROCK_HEADER, BEDROCK_HEADER_HASH, BEDROCK_HEADER_TTD};
use reth_primitives::{
BlockBody, BlockNumber, Header, SealedBlock, SealedBlockWithSenders, SealedHeader,
StaticFileSegment, U256,
};
use reth_provider::{
providers::StaticFileProvider, BlockWriter, DatabaseProviderRW, StageCheckpointWriter,
StaticFileWriter,
};
use reth_stages::{StageCheckpoint, StageId};
use tracing::info;
/// Creates a dummy chain (with no transactions) up to the last OVM block and appends the
/// first valid Bedrock block.
pub(crate) fn setup_op_mainnet_without_ovm<DB: Database>(
provider_rw: &DatabaseProviderRW<DB>,
static_file_provider: &StaticFileProvider,
) -> Result<(), eyre::Error> {
info!(target: "reth::cli", "Setting up dummy OVM chain before importing state.");
// Write OVM dummy data up to `BEDROCK_HEADER - 1` block
append_dummy_chain(static_file_provider, BEDROCK_HEADER.number - 1)?;
info!(target: "reth::cli", "Appending Bedrock block.");
append_bedrock_block(provider_rw, static_file_provider)?;
for stage in StageId::ALL {
provider_rw.save_stage_checkpoint(stage, StageCheckpoint::new(BEDROCK_HEADER.number))?;
}
info!(target: "reth::cli", "Set up finished.");
Ok(())
}
/// Appends the first bedrock block.
///
/// By appending it, static file writer also verifies that all segments are at the same
/// height.
fn append_bedrock_block<DB: Database>(
provider_rw: &DatabaseProviderRW<DB>,
sf_provider: &StaticFileProvider,
) -> Result<(), eyre::Error> {
provider_rw.insert_block(
SealedBlockWithSenders::new(
SealedBlock::new(
SealedHeader::new(BEDROCK_HEADER, BEDROCK_HEADER_HASH),
BlockBody::default(),
),
vec![],
)
.expect("no senders or txes"),
)?;
sf_provider.latest_writer(StaticFileSegment::Headers)?.append_header(
&BEDROCK_HEADER,
BEDROCK_HEADER_TTD,
&BEDROCK_HEADER_HASH,
)?;
sf_provider
.latest_writer(StaticFileSegment::Receipts)?
.increment_block(BEDROCK_HEADER.number)?;
sf_provider
.latest_writer(StaticFileSegment::Transactions)?
.increment_block(BEDROCK_HEADER.number)?;
Ok(())
}
/// Creates a dummy chain with no transactions/receipts up to `target_height` block inclusive.
///
/// * Headers: It will push an empty block.
/// * Transactions: It will not push any tx, only increments the end block range.
/// * Receipts: It will not push any receipt, only increments the end block range.
fn append_dummy_chain(
sf_provider: &StaticFileProvider,
target_height: BlockNumber,
) -> Result<(), eyre::Error> {
let (tx, rx) = std::sync::mpsc::channel();
// Spawn jobs for incrementing the block end range of transactions and receipts
for segment in [StaticFileSegment::Transactions, StaticFileSegment::Receipts] {
let tx_clone = tx.clone();
let provider = sf_provider.clone();
std::thread::spawn(move || {
let result = provider.latest_writer(segment).and_then(|mut writer| {
for block_num in 1..=target_height {
writer.increment_block(block_num)?;
}
Ok(())
});
tx_clone.send(result).unwrap();
});
}
// Spawn job for appending empty headers
let provider = sf_provider.clone();
std::thread::spawn(move || {
let mut empty_header = Header::default();
let result = provider.latest_writer(StaticFileSegment::Headers).and_then(|mut writer| {
for block_num in 1..=target_height {
// TODO: should we fill with real parent_hash?
empty_header.number = block_num;
writer.append_header(&empty_header, U256::ZERO, &B256::ZERO)?;
}
Ok(())
});
tx.send(result).unwrap();
});
// Catches any StaticFileWriter error.
while let Ok(r) = rx.recv() {
r?;
}
// If, for any reason, rayon crashes this verifies if all segments are at the same
// target_height.
for segment in
[StaticFileSegment::Headers, StaticFileSegment::Receipts, StaticFileSegment::Transactions]
{
assert_eq!(
sf_provider.latest_writer(segment)?.user_header().block_end(),
Some(target_height),
"Static file segment {segment} was unsuccessful advancing its block height."
);
}
Ok(())
}

View File

@ -0,0 +1,80 @@
//! Command that initializes the node from a genesis file.
use clap::Parser;
use reth_chainspec::ChainSpec;
use reth_cli::chainspec::ChainSpecParser;
use reth_cli_commands::common::{AccessRights, Environment};
use reth_db_common::init::init_from_state_dump;
use reth_node_builder::NodeTypesWithEngine;
use reth_optimism_primitives::bedrock::BEDROCK_HEADER;
use reth_provider::{
BlockNumReader, ChainSpecProvider, StaticFileProviderFactory, StaticFileWriter,
};
use std::{fs::File, io::BufReader};
use tracing::info;
mod bedrock;
/// Initializes the database with the genesis block.
#[derive(Debug, Parser)]
pub struct InitStateCommandOp<C: ChainSpecParser> {
#[command(flatten)]
init_state: reth_cli_commands::init_state::InitStateCommand<C>,
/// **Optimism Mainnet Only**
///
/// Specifies whether to initialize the state without relying on OVM historical data.
///
/// When enabled, and before inserting the state, it creates a dummy chain up to the last OVM
/// block (#105235062) (14GB / 90 seconds). It then, appends the Bedrock block.
///
/// - **Note**: **Do not** import receipts and blocks beforehand, or this will fail or be
/// ignored.
#[arg(long, default_value = "false")]
without_ovm: bool,
}
impl<C: ChainSpecParser<ChainSpec = ChainSpec>> InitStateCommandOp<C> {
/// Execute the `init` command
pub async fn execute<N: NodeTypesWithEngine<ChainSpec = C::ChainSpec>>(
self,
) -> eyre::Result<()> {
info!(target: "reth::cli", "Reth init-state starting");
let Environment { config, provider_factory, .. } =
self.init_state.env.init::<N>(AccessRights::RW)?;
let static_file_provider = provider_factory.static_file_provider();
let provider_rw = provider_factory.provider_rw()?;
// OP-Mainnet may want to bootstrap a chain without OVM historical data
if provider_factory.chain_spec().is_optimism_mainnet() && self.without_ovm {
let last_block_number = provider_rw.last_block_number()?;
if last_block_number == 0 {
bedrock::setup_op_mainnet_without_ovm(&provider_rw, &static_file_provider)?;
// SAFETY: it's safe to commit static files, since in the event of a crash, they
// will be unwinded according to database checkpoints.
//
// Necessary to commit, so the BEDROCK_HEADER is accessible to provider_rw and
// init_state_dump
static_file_provider.commit()?;
} else if last_block_number > 0 && last_block_number < BEDROCK_HEADER.number {
return Err(eyre::eyre!(
"Data directory should be empty when calling init-state with --without-ovm."
))
}
}
info!(target: "reth::cli", "Initiating state dump");
let reader = BufReader::new(File::open(self.init_state.state)?);
let hash = init_from_state_dump(reader, &provider_rw.0, config.stages.etl)?;
provider_rw.commit()?;
info!(target: "reth::cli", hash = ?hash, "Genesis block written");
Ok(())
}
}

View File

@ -5,7 +5,7 @@ use import_receipts::ImportReceiptsOpCommand;
use reth_chainspec::ChainSpec;
use reth_cli::chainspec::ChainSpecParser;
use reth_cli_commands::{
config_cmd, db, dump_genesis, init_cmd, init_state,
config_cmd, db, dump_genesis, init_cmd,
node::{self, NoArgs},
p2p, prune, recover, stage,
};
@ -15,6 +15,7 @@ use std::fmt;
mod build_pipeline;
pub mod import;
pub mod import_receipts;
pub mod init_state;
/// Commands to be executed
#[derive(Debug, Subcommand)]
@ -30,7 +31,7 @@ pub enum Commands<
Init(init_cmd::InitCommand<Spec>),
/// Initialize the database from a state dump file.
#[command(name = "init-state")]
InitState(init_state::InitStateCommand<Spec>),
InitState(init_state::InitStateCommandOp<Spec>),
/// This syncs RLP encoded OP blocks below Bedrock from a file, without executing.
#[command(name = "import-op")]
ImportOp(ImportOpCommand<Spec>),

View File

@ -10,3 +10,8 @@ description = "OP primitive types"
[lints]
workspace = true
[dependencies]
reth-primitives.workspace = true
reth-primitives-traits.workspace = true
alloy-primitives.workspace = true

View File

@ -0,0 +1,98 @@
//! OP mainnet bedrock related data.
use alloy_primitives::{b256, bloom, bytes, B256, U256};
use reth_primitives::{address, Header};
use reth_primitives_traits::constants::EMPTY_OMMER_ROOT_HASH;
/// Transaction 0x9ed8f713b2cc6439657db52dcd2fdb9cc944915428f3c6e2a7703e242b259cb9 in block 985,
/// replayed in blocks:
///
/// 19 022
/// 45 036
pub const TX_BLOCK_985: [u64; 2] = [19_022, 45_036];
/// Transaction 0xc033250c5a45f9d104fc28640071a776d146d48403cf5e95ed0015c712e26cb6 in block
/// 123 322, replayed in block:
///
/// 123 542
pub const TX_BLOCK_123_322: u64 = 123_542;
/// Transaction 0x86f8c77cfa2b439e9b4e92a10f6c17b99fce1220edf4001e4158b57f41c576e5 in block
/// 1 133 328, replayed in blocks:
///
/// 1 135 391
/// 1 144 468
pub const TX_BLOCK_1_133_328: [u64; 2] = [1_135_391, 1_144_468];
/// Transaction 0x3cc27e7cc8b7a9380b2b2f6c224ea5ef06ade62a6af564a9dd0bcca92131cd4e in block
/// 1 244 152, replayed in block:
///
/// 1 272 994
pub const TX_BLOCK_1_244_152: u64 = 1_272_994;
/// The six blocks with replayed transactions.
pub const BLOCK_NUMS_REPLAYED_TX: [u64; 6] = [
TX_BLOCK_985[0],
TX_BLOCK_985[1],
TX_BLOCK_123_322,
TX_BLOCK_1_133_328[0],
TX_BLOCK_1_133_328[1],
TX_BLOCK_1_244_152,
];
/// Returns `true` if transaction is the second or third appearance of the transaction. The blocks
/// with replayed transaction happen to only contain the single transaction.
pub fn is_dup_tx(block_number: u64) -> bool {
if block_number > BLOCK_NUMS_REPLAYED_TX[5] {
return false
}
// these blocks just have one transaction!
if BLOCK_NUMS_REPLAYED_TX.contains(&block_number) {
return true
}
false
}
/// Bedrock hash on Optimism Mainnet.
pub const BEDROCK_HEADER_HASH: B256 =
b256!("dbf6a80fef073de06add9b0d14026d6e5a86c85f6d102c36d3d8e9cf89c2afd3");
/// Bedrock on Optimism Mainnet. (`105_235_063`)
pub const BEDROCK_HEADER: Header = Header {
difficulty: U256::ZERO,
extra_data: bytes!("424544524f434b"),
gas_limit: 30000000,
gas_used: 0,
logs_bloom: bloom!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
nonce: 0,
number: 105235063,
parent_hash: b256!("21a168dfa5e727926063a28ba16fd5ee84c814e847c81a699c7a0ea551e4ca50"),
receipts_root: b256!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"),
state_root: b256!("920314c198da844a041d63bf6cbe8b59583165fd2229d1b3f599da812fd424cb"),
timestamp: 1686068903,
transactions_root: b256!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"),
ommers_hash: EMPTY_OMMER_ROOT_HASH,
beneficiary: address!("4200000000000000000000000000000000000011"),
withdrawals_root: None,
mix_hash: B256::ZERO,
base_fee_per_gas: Some(0x3b9aca00),
blob_gas_used: None,
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_root: None,
};
/// Bedrock total difficulty on Optimism Mainnet.
pub const BEDROCK_HEADER_TTD: U256 = U256::ZERO;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bedrock_header() {
assert_eq!(BEDROCK_HEADER.hash_slow(), BEDROCK_HEADER_HASH);
}
}

View File

@ -1,52 +0,0 @@
//! Replayed OP mainnet OVM transactions (in blocks below Bedrock).
/// Transaction 0x9ed8f713b2cc6439657db52dcd2fdb9cc944915428f3c6e2a7703e242b259cb9 in block 985,
/// replayed in blocks:
///
/// 19 022
/// 45 036
pub const TX_BLOCK_985: [u64; 2] = [19_022, 45_036];
/// Transaction 0xc033250c5a45f9d104fc28640071a776d146d48403cf5e95ed0015c712e26cb6 in block
/// 123 322, replayed in block:
///
/// 123 542
pub const TX_BLOCK_123_322: u64 = 123_542;
/// Transaction 0x86f8c77cfa2b439e9b4e92a10f6c17b99fce1220edf4001e4158b57f41c576e5 in block
/// 1 133 328, replayed in blocks:
///
/// 1 135 391
/// 1 144 468
pub const TX_BLOCK_1_133_328: [u64; 2] = [1_135_391, 1_144_468];
/// Transaction 0x3cc27e7cc8b7a9380b2b2f6c224ea5ef06ade62a6af564a9dd0bcca92131cd4e in block
/// 1 244 152, replayed in block:
///
/// 1 272 994
pub const TX_BLOCK_1_244_152: u64 = 1_272_994;
/// The six blocks with replayed transactions.
pub const BLOCK_NUMS_REPLAYED_TX: [u64; 6] = [
TX_BLOCK_985[0],
TX_BLOCK_985[1],
TX_BLOCK_123_322,
TX_BLOCK_1_133_328[0],
TX_BLOCK_1_133_328[1],
TX_BLOCK_1_244_152,
];
/// Returns `true` if transaction is the second or third appearance of the transaction. The blocks
/// with replayed transaction happen to only contain the single transaction.
pub fn is_dup_tx(block_number: u64) -> bool {
if block_number > BLOCK_NUMS_REPLAYED_TX[5] {
return false
}
// these blocks just have one transaction!
if BLOCK_NUMS_REPLAYED_TX.contains(&block_number) {
return true
}
false
}

View File

@ -7,4 +7,4 @@
)]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
pub mod bedrock_import;
pub mod bedrock;

View File

@ -326,29 +326,27 @@ where
/// It's similar to [`init_genesis`] but supports importing state too big to fit in memory, and can
/// be set to the highest block present. One practical usecase is to import OP mainnet state at
/// bedrock transition block.
pub fn init_from_state_dump<PF>(
pub fn init_from_state_dump<Provider>(
mut reader: impl BufRead,
factory: PF,
provider_rw: &Provider,
etl_config: EtlConfig,
) -> eyre::Result<B256>
where
PF: DatabaseProviderFactory
+ StaticFileProviderFactory
+ ChainSpecProvider<ChainSpec = ChainSpec>
+ BlockHashReader
Provider: DBProvider<Tx: DbTxMut>
+ BlockNumReader
+ HeaderProvider,
PF::ProviderRW: StageCheckpointWriter
+ BlockHashReader
+ ChainSpecProvider<ChainSpec = ChainSpec>
+ StageCheckpointWriter
+ HistoryWriter
+ HeaderProvider
+ HashingWriter
+ StateChangeWriter
+ TrieWriter
+ AsRef<PF::ProviderRW>,
+ AsRef<Provider>,
{
let block = factory.last_block_number()?;
let hash = factory.block_hash(block)?.unwrap();
let expected_state_root = factory
let block = provider_rw.last_block_number()?;
let hash = provider_rw.block_hash(block)?.unwrap();
let expected_state_root = provider_rw
.header_by_number(block)?
.ok_or(ProviderError::HeaderNotFound(block.into()))?
.state_root;
@ -370,7 +368,7 @@ where
debug!(target: "reth::cli",
block,
chain=%factory.chain_spec().chain,
chain=%provider_rw.chain_spec().chain,
"Initializing state at block"
);
@ -378,11 +376,10 @@ where
let collector = parse_accounts(&mut reader, etl_config)?;
// write state to db
let provider_rw = factory.database_provider_rw()?;
dump_state(collector, &provider_rw, block)?;
dump_state(collector, provider_rw, block)?;
// compute and compare state root. this advances the stage checkpoints.
let computed_state_root = compute_state_root(&provider_rw)?;
let computed_state_root = compute_state_root(provider_rw)?;
if computed_state_root == expected_state_root {
info!(target: "reth::cli",
?computed_state_root,
@ -407,8 +404,6 @@ where
provider_rw.save_stage_checkpoint(stage, StageCheckpoint::new(block))?;
}
provider_rw.commit()?;
Ok(hash)
}

View File

@ -18,7 +18,7 @@ use crate::{
use alloy_primitives::{keccak256, Address, BlockHash, BlockNumber, TxHash, TxNumber, B256, U256};
use itertools::{izip, Itertools};
use rayon::slice::ParallelSliceMut;
use reth_chainspec::{ChainInfo, ChainSpec, EthereumHardforks};
use reth_chainspec::{ChainInfo, ChainSpec, ChainSpecProvider, EthereumHardforks};
use reth_db::{
cursor::DbDupCursorRW, tables, BlockNumberList, PlainAccountState, PlainStorageState,
};
@ -1595,6 +1595,14 @@ impl<TX: DbTxMut + DbTx> DatabaseProvider<TX> {
}
}
impl<TX: DbTx> ChainSpecProvider for DatabaseProvider<TX> {
type ChainSpec = ChainSpec;
fn chain_spec(&self) -> Arc<ChainSpec> {
self.chain_spec.clone()
}
}
impl<TX: DbTx> AccountReader for DatabaseProvider<TX> {
fn basic_account(&self, address: Address) -> ProviderResult<Option<Account>> {
Ok(self.tx.get::<tables::PlainAccountState>(address)?)