Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bc4d4e1
test: :test_tube: add TDD failing test
manuelmauro Jan 15, 2026
105efa2
refactor: :recycle: use mapping-sync as single source of truth for et…
manuelmauro Jan 15, 2026
7dd7115
fix: :bug: fix missing args
manuelmauro Jan 16, 2026
2f6a1c0
test: :white_check_mark: redefine waitForBlock as a polling function
manuelmauro Jan 19, 2026
bbb0d35
style: :art: fmt
manuelmauro Jan 19, 2026
8212696
fix: :bug: wait for the right block number
manuelmauro Jan 19, 2026
e50efb6
revert: :fire: remove temporary delay
manuelmauro Jan 21, 2026
a93fccc
chore: :package: update package lock
manuelmauro Jan 21, 2026
52360b4
test: :white_check_mark: update test timeouts
manuelmauro Jan 21, 2026
3cd7389
test: :white_check_mark: remove explicit nonce to fix EIP-7702 test r…
manuelmauro Jan 21, 2026
793f3b0
fix: bypass ethers.js nonce caching in EIP-7702 tests
manuelmauro Jan 21, 2026
fd03be5
fix: :bug: verify mapped blocks against canonical chain during reorgs
manuelmauro Jan 26, 2026
45e0841
revert: :fire: remove LATEST_INDEXED_BLOCK
manuelmauro Jan 26, 2026
404fed3
fix: :bug: correctly error instead of defaulting to block number 0
manuelmauro Feb 2, 2026
fbe583d
fix: :bug: verify all non-genesis blocks
manuelmauro Feb 2, 2026
bf08853
test: :white_check_mark: add error handling to waitForBlock
manuelmauro Feb 2, 2026
3d2f95c
fix: :bug: use consistent behavior for eth_getBlockByNumber("latest")…
manuelmauro Feb 2, 2026
091f2ae
feat: :sparkles: add KV store migration
manuelmauro Feb 3, 2026
42bc7b8
fix: :bug: do not default to 0
manuelmauro Feb 3, 2026
ddfd4d0
feat: :loud_sound: log processed entries in parity db migration
manuelmauro Feb 3, 2026
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
3 changes: 3 additions & 0 deletions client/api/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ pub trait Backend<Block: BlockT>: Send + Sync {
ethereum_block_hash: &H256,
) -> Result<Option<Vec<Block::Hash>>, String>;

/// Get the ethereum block hash for a given block number.
async fn block_hash_by_number(&self, block_number: u64) -> Result<Option<H256>, String>;

/// Get the transaction metadata with the given ethereum block hash.
async fn transaction_metadata(
&self,
Expand Down
32 changes: 29 additions & 3 deletions client/cli/src/frontier_db_cmd/mapping_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use serde::Deserialize;
// Substrate
use sp_api::ProvideRuntimeApi;
use sp_blockchain::HeaderBackend;
use sp_runtime::traits::Block as BlockT;
use sp_runtime::traits::{Block as BlockT, Header as HeaderT, UniqueSaturatedInto};
// Frontier
use fp_rpc::EthereumRuntimeRPCApi;

Expand Down Expand Up @@ -103,7 +103,20 @@ where
ethereum_transaction_hashes: existing_transaction_hashes,
};

self.backend.mapping().write_hashes(commitment)?;
// Get block number from header
let block_number: u64 = (*self
.client
.header(*substrate_block_hash)
.map_err(|e| format!("{e:?}"))?
.ok_or_else(|| {
format!("Header not found for block {substrate_block_hash:?}")
})?
.number())
.unique_saturated_into();

self.backend
.mapping()
.write_hashes(commitment, block_number)?;
} else {
return Err(self.key_not_empty_error(key));
}
Expand Down Expand Up @@ -161,7 +174,20 @@ where
ethereum_transaction_hashes: existing_transaction_hashes,
};

self.backend.mapping().write_hashes(commitment)?;
// Get block number from header
let block_number: u64 = (*self
.client
.header(*substrate_block_hash)
.map_err(|e| format!("{e:?}"))?
.ok_or_else(|| {
format!("Header not found for block {substrate_block_hash:?}")
})?
.number())
.unique_saturated_into();

self.backend
.mapping()
.write_hashes(commitment, block_number)?;
}
}
_ => return Err(self.key_value_error(key, value)),
Expand Down
68 changes: 64 additions & 4 deletions client/db/src/kv/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub use sc_client_db::DatabaseSource;
use sp_blockchain::HeaderBackend;
use sp_core::{H160, H256};
pub use sp_database::Database;
use sp_runtime::traits::Block as BlockT;
use sp_runtime::traits::{Block as BlockT, UniqueSaturatedInto};
// Frontier
use fc_api::{FilteredLog, TransactionMetadata};
use fp_storage::{EthereumStorageSchema, PALLET_ETHEREUM_SCHEMA_CACHE};
Expand All @@ -49,12 +49,13 @@ pub struct DatabaseSettings {
}

pub(crate) mod columns {
pub const NUM_COLUMNS: u32 = 4;
pub const NUM_COLUMNS: u32 = 5;

pub const META: u32 = 0;
pub const BLOCK_MAPPING: u32 = 1;
pub const TRANSACTION_MAPPING: u32 = 2;
pub const SYNCED_MAPPING: u32 = 3;
pub const BLOCK_NUMBER_MAPPING: u32 = 4;
}

pub mod static_keys {
Expand All @@ -78,6 +79,10 @@ impl<Block: BlockT, C: HeaderBackend<Block>> fc_api::Backend<Block> for Backend<
self.mapping().block_hash(ethereum_block_hash)
}

async fn block_hash_by_number(&self, block_number: u64) -> Result<Option<H256>, String> {
self.mapping().block_hash_by_number(block_number)
}

async fn transaction_metadata(
&self,
ethereum_transaction_hash: &H256,
Expand All @@ -95,7 +100,39 @@ impl<Block: BlockT, C: HeaderBackend<Block>> fc_api::Backend<Block> for Backend<
}

async fn latest_block_hash(&self) -> Result<Block::Hash, String> {
Ok(self.client.info().best_hash)
// Return the latest block hash that is both indexed AND on the canonical chain.
// This prevents returning stale data during reorgs.
//
// Note: During initial sync or after restart while mapping-sync catches up,
// this returns the genesis block hash. This is consistent with Geth's behavior
// where eth_getBlockByNumber("latest") returns block 0 during initial sync.
// Users can check sync status via eth_syncing to determine if the node is
// still catching up.
let best_number: u64 = self.client.info().best_number.unique_saturated_into();

// Get the canonical hash for verification.
let canonical_hash = self
.client
.hash(best_number.unique_saturated_into())
.map_err(|e| format!("{e:?}"))?;

// Query mapping-sync for the ethereum block hash at best_number
if let Some(eth_hash) = self.mapping.block_hash_by_number(best_number)? {
// Get the substrate block hash(es) for this ethereum block hash
if let Some(substrate_hashes) = self.mapping.block_hash(&eth_hash)? {
// Verify the mapped hash is on the canonical chain.
// During a reorg, the mapping may point to a reorged-out block.
if let Some(canonical) = canonical_hash {
if substrate_hashes.contains(&canonical) {
return Ok(canonical);
}
}
// Mapping exists but is stale (reorg happened) - treat as not indexed
}
}

// Block not indexed yet or stale - return genesis
Ok(self.client.info().genesis_hash)
}
}

Expand Down Expand Up @@ -310,7 +347,11 @@ impl<Block: BlockT> MappingDb<Block> {
Ok(())
}

pub fn write_hashes(&self, commitment: MappingCommitment<Block>) -> Result<(), String> {
pub fn write_hashes(
&self,
commitment: MappingCommitment<Block>,
block_number: u64,
) -> Result<(), String> {
let _lock = self.write_lock.lock();

let mut transaction = sp_database::Transaction::new();
Expand All @@ -337,6 +378,13 @@ impl<Block: BlockT> MappingDb<Block> {
&substrate_hashes.encode(),
);

// Write block number -> ethereum block hash mapping
transaction.set(
columns::BLOCK_NUMBER_MAPPING,
&block_number.encode(),
&commitment.ethereum_block_hash.encode(),
);

for (i, ethereum_transaction_hash) in commitment
.ethereum_transaction_hashes
.into_iter()
Expand Down Expand Up @@ -365,4 +413,16 @@ impl<Block: BlockT> MappingDb<Block> {

Ok(())
}

pub fn block_hash_by_number(&self, block_number: u64) -> Result<Option<H256>, String> {
match self
.db
.get(columns::BLOCK_NUMBER_MAPPING, &block_number.encode())
{
Some(raw) => Ok(Some(
H256::decode(&mut &raw[..]).map_err(|e| format!("{e:?}"))?,
)),
None => Ok(None),
}
}
}
Loading
Loading