Skip to content

feat: Starknet inspector#2084

Merged
netrome merged 2 commits intomainfrom
2054-foreign-chain-inspector-for-starknet
Feb 16, 2026
Merged

feat: Starknet inspector#2084
netrome merged 2 commits intomainfrom
2054-foreign-chain-inspector-for-starknet

Conversation

@netrome
Copy link
Collaborator

@netrome netrome commented Feb 13, 2026

closes #2054

@netrome netrome linked an issue Feb 13, 2026 that may be closed by this pull request
Comment on lines +239 to +242
pub enum StarknetFinality {
AcceptedOnL2,
AcceptedOnL1,
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starknet has a few more statuses we could support (see https://docs.starknet.io/learn/protocol/transactions#transaction-statuses), but I think this is a sensible start.

@netrome netrome force-pushed the 2054-foreign-chain-inspector-for-starknet branch from f7d42a3 to c26c10d Compare February 13, 2026 14:47
Comment on lines +77 to +78
where
Client: ClientT + Send,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These trait bounds are redundant, and should be removed. But this mirrors how it's done in the other extractors so I prefer keeping it consistent for now.

}

#[test]
fn test_as_hex_with_prefix() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I'd normally push for the new naming convention here (e.g. something like as_hex_with_prefix__should_result_in_prefixed_hex), this would stand out a bit too much in this test module - and this test is trivial so I chose to keep it this way for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This config update is relatively stand-alone and could be broken out, but it felt nice to add it since it enables this to be verified end to end.

})
.collect::<anyhow::Result<_>>()?
}
dtos::ForeignChainRpcRequest::Starknet(request) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the config change, this wraps up the feature end to end so it's nice to add but is separate from the inspector.

@netrome netrome marked this pull request as ready for review February 13, 2026 21:09
@netrome netrome force-pushed the 2054-foreign-chain-inspector-for-starknet branch from c22269d to 9898996 Compare February 13, 2026 21:10
@netrome netrome self-assigned this Feb 13, 2026
@claude
Copy link

claude bot commented Feb 13, 2026

Code Review

Found several critical issues that need fixing before merge:

1. ❌ Unsafe Wildcard Pattern Matching (Production Safety)

Location: crates/node/src/providers/verify_foreign_tx/sign.rs:185-188

let finality = match request.finality {
    dtos::StarknetFinality::AcceptedOnL2 => StarknetFinality::AcceptedOnL2,
    dtos::StarknetFinality::AcceptedOnL1 => StarknetFinality::AcceptedOnL1,
    _ => bail\!("unknown starknet finality level"),  // ❌ Dangerous wildcard
};

Problem: StarknetFinality is marked #[non_exhaustive]. Using a wildcard pattern means new variants added to the contract interface will compile but fail at runtime. This is dangerous during rolling updates in multi-cluster deployments.

Fix: Remove the wildcard and match exhaustively:

let finality = match request.finality {
    dtos::StarknetFinality::AcceptedOnL2 => StarknetFinality::AcceptedOnL2,
    dtos::StarknetFinality::AcceptedOnL1 => StarknetFinality::AcceptedOnL1,
};

The compiler will force you to update this code when new variants are added.


2. ❌ Same Issue with Extractor Matching

Location: crates/node/src/providers/verify_foreign_tx/sign.rs:190-197

.map(|extractor| match extractor {
    dtos::StarknetExtractor::BlockHash => Ok(StarknetExtractor::BlockHash),
    _ => bail\!("unknown extractor found"),  // ❌ Same problem
})

StarknetExtractor is also #[non_exhaustive]. Remove the wildcard pattern.


3. ⚠️ Unnecessary Clone in Hot Path (Performance)

Location: crates/node/src/providers/verify_foreign_tx/sign.rs:176

let rpc_url = starknet_provider_config.rpc_url.clone();

This clones the entire URL string unnecessarily. Use the rpc_url() method instead which returns Cow<'_, str>:

let http_client = foreign_chain_inspector::build_http_client(
    starknet_provider_config.rpc_url().into_owned(),
    starknet_provider_config.auth.clone().try_into()?,
)?;

4. ℹ️ Missing Execution Status Differentiation

Location: crates/foreign-chain-inspector/src/starknet/inspector.rs:48-50

The current check treats Reverted and Rejected execution statuses the same way:

if rpc_response.execution_status \!= StarknetExecutionStatus::Succeeded {
    return Err(ForeignChainInspectionError::TransactionFailed);
}

Consider adding logging to distinguish these cases for better debugging (Rejected = pre-execution failure, Reverted = execution failure).


Priority: Items #1 and #2 are critical and must be fixed before merge to prevent runtime failures during version upgrades.

⚠️

@netrome
Copy link
Collaborator Author

netrome commented Feb 13, 2026

Sorry Claude, but this wasn't your best review. 1. and 2. are needed since the enums are non-exhaustive as you already noted. Also the existing bitcoin logic follows the same pattern. 3. is wrong since this is a string and 4. isn't really important right now.

@netrome netrome requested review from DSharifi and gilcu3 February 13, 2026 21:25
Comment on lines +60 to +79
fn deserialize_starknet_felt_hash<'de, D>(
deserializer: D,
) -> Result<TransportStarknetBlockHash, D::Error>
where
D: Deserializer<'de>,
{
let hash = String::deserialize(deserializer)?;
let stripped = hash.strip_prefix("0x").unwrap_or(&hash);

if stripped.len() > 64 {
return Err(serde::de::Error::custom(format!(
"felt hex string too long: {hash}"
)));
}

let padded = format!("{stripped:0>64}");
padded.parse().map_err(|error| {
serde::de::Error::custom(format!("invalid starknet felt hash {hash}: {error}"))
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the starknet response schema is the same as EVM/Ethereum by prefixing with 0x. You can reuse the ethereum_types::H256 crate to avoid these manual serialization implementations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 4c13d4a - unfortunately starknet allows stripped-prefix hashes so we still have some custom deserialization logic. But it's cleaner still.

Another alternative would be to drag in some starknet-specific dependency for this but that feels overkill for just hex deserialization.

Copy link
Contributor

@DSharifi DSharifi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly looks good.

But I want to discuss if it makes sense to implement the manual (de)serialization of hex strings with 0x prefix.

They (starknet) are reusing a lot of the same format from EVM from their official spec, such as this 0x prefixing and the name of the methods https://github.com/starkware-libs/starknet-specs/blob/master/starknet_vs_ethereum_node_apis.md

In that case, I think we should reuse the ethereum_types crates we already depend on from parity which provides all this (de)serialization logic built in.

@netrome netrome force-pushed the 2054-foreign-chain-inspector-for-starknet branch from 9106a16 to 4c13d4a Compare February 14, 2026 21:54
@netrome netrome requested a review from DSharifi February 14, 2026 22:07
Copy link
Contributor

@gilcu3 gilcu3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Requesting changes because we have a problem with the manual test:

        FAIL [   0.105s] foreign-chain-inspector::starknet_rpc_manual inspector_extracts_block_hash_against_live_rpc_provider
  stdout ───

    running 1 test
    test inspector_extracts_block_hash_against_live_rpc_provider ... FAILED

    failures:

    failures:
        inspector_extracts_block_hash_against_live_rpc_provider

    test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

  stderr ───

    thread 'inspector_extracts_block_hash_against_live_rpc_provider' panicked at crates/foreign-chain-inspector/tests/starknet_rpc_manual.rs:26:10:
    latest block should be fetched: ParseError(Error("invalid length 63, expected a (both 0x-prefixed or not) hex string or byte array containing 32 bytes", line: 1, column: 107))
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

@@ -0,0 +1,17 @@
use mpc_primitives::hash::Hash32;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really have a hard time figuring when a type should come from one crate or another. In this case I naively expected this to come from the contract-interface crate, but it comes from mpc-primitives instead. Do we have a well-defined boundary between these two crates? I am all in for merging them if not

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think this isn't completely obvious for some cases, here's how I'd do it:

Complex types used in the contract interface -> contract-interface
Primitive types used for things unrelated to the interface -> mpc-primitives
Primitive types only used for the interface -> contract-interface

I think the last one is the most interesting case, because the two above are fairly obvious. There are crates that may need some primitives that don't rely on the contract interface and those primitives should obviously live in mpc-primitives. And anything that's not a primitive should not live there.

For the last case, both would work so I have no strong opinion but here it's better to maintain locality if the primitive isn't expected to be used anywhere else.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation. What is a "primitive" type in this context? Aren't we using it as a synonym of simply "mostly non-interface type"? What is the difference between the crate node-types and mpc-primitives ? I guess that node-types is only about the node, but then why would it be outside of the node?

@netrome
Copy link
Collaborator Author

netrome commented Feb 16, 2026

Thank you!

Requesting changes because we have a problem with the manual test:

        FAIL [   0.105s] foreign-chain-inspector::starknet_rpc_manual inspector_extracts_block_hash_against_live_rpc_provider
  stdout ───

    running 1 test
    test inspector_extracts_block_hash_against_live_rpc_provider ... FAILED

    failures:

    failures:
        inspector_extracts_block_hash_against_live_rpc_provider

    test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

  stderr ───

    thread 'inspector_extracts_block_hash_against_live_rpc_provider' panicked at crates/foreign-chain-inspector/tests/starknet_rpc_manual.rs:26:10:
    latest block should be fetched: ParseError(Error("invalid length 63, expected a (both 0x-prefixed or not) hex string or byte array containing 32 bytes", line: 1, column: 107))
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Thanks for the observation. This passed before, so it has to do with the hash type change probably. I'll fix it.

@netrome netrome force-pushed the 2054-foreign-chain-inspector-for-starknet branch from a5f59aa to 504af02 Compare February 16, 2026 08:07
@netrome netrome added this pull request to the merge queue Feb 16, 2026
Merged via the queue into main with commit 026de4a Feb 16, 2026
9 checks passed
@netrome netrome deleted the 2054-foreign-chain-inspector-for-starknet branch February 16, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

foreign chain inspector for Starknet

3 participants