Skip to content

Pharos Network Integration#628

Open
dharjeezy wants to merge 37 commits intomainfrom
dami/pharos-network
Open

Pharos Network Integration#628
dharjeezy wants to merge 37 commits intomainfrom
dami/pharos-network

Conversation

@dharjeezy
Copy link
Copy Markdown
Contributor

@dharjeezy dharjeezy commented Jan 13, 2026

This PR introduces the integration to Pharos network for Hyperbridge.

  • Pharos network verifier
  • ISMP consensus
  • Pharos Prover
  • Tesseract Relayer Intergation
  • Pharos State Machine

closes #593

return Err(Error::NoParticipants);
}

let aggregate_pubkey = bsc_verifier::aggregate_public_keys(participants);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we have this method in s crypto utils, if not let's move it there

/// Compute the hash of a block header using RLP encoding and Keccak256.
///
/// This follows the standard Ethereum header hash computation.
pub fn compute_header_hash<H: Keccak256>(header: &CodecHeader) -> H256 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We already have this in the geth primitives, there's a hash function implemented on the header

}

/// Get values from storage proof.
fn get_values_from_proof<H: Keccak256 + Send + Sync>(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Import all these EVM utility methods from the evm state machine instead of duplicating

#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
pub struct ValidatorSetProof {
/// The new validator set
pub validator_set: ValidatorSet,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
pub validator_set: ValidatorSet,

The proof doesn't need to come with the validator set, we can trust the values we decode from the proof

let storage_root = H256::from_slice(account.storage_root.as_slice());

let layout = StakingContractLayout::default();
let validator_count = proof.validator_set.validators.len() as u64;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can just have a value activePoolIdTotalCount on the ValidatorSetProof

use existing apis
@dharjeezy dharjeezy requested a review from Wizdave97 January 15, 2026 14:38
remove redundant hashing
use evm config url
) {
map.insert(key, Some(value_hash.to_vec()));
} else {
map.insert(key, None);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i'm not convinced this is correct

error on missing keys
proof: Vec<u8>,
) -> Result<(Vec<u8>, ismp::consensus::VerifiedCommitments), Error> {
let update = VerifierStateUpdate::decode(&mut &proof[..])
.map_err(|e| Error::AnyHow(anyhow::anyhow!("{:?}", e).into()))?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

doesn't parity_scale_code::Error implement core::Error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

parity_scale_codec::Error only implements std::error::Error which is gated behind the std feature, not core::error::Error actually.

Copy link
Copy Markdown
Member

@Wizdave97 Wizdave97 left a comment

Choose a reason for hiding this comment

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

@dharjeezy

The plan is now complete with the critical SkipEmpty attack finding properly documented against the Pharos docs. This is a research/review task, not an implementation task, so I should present the findings directly rather than calling ExitPlanMode.

Here's my comprehensive review of PR #628: Pharos Network Integration:


Key Architecture

The PR adds a complete Pharos network integration across 6 modules (~3000+ lines, 23 new files):

  • Primitives: Types + hexary hash tree SPV verification (SHA-256, not Keccak)
  • Verifier: BLS-based block verification + validator set proof from staking contract
  • Prover: RPC client with two-phase concurrent fetching
  • ISMP Client: ConsensusClient + fraud proofs
  • State Machine: Existence + non-existence proof verification for ISMP
  • Tesseract Relayer: Epoch-by-epoch syncing loop

Pharos uses a flat trie (storage proofs verify directly against state root) with SkipEmpty hashing (SHA-256 over only non-empty slots).


CRITICAL Finding

(a) SkipEmpty Attack in Non-Existence Proofs (spv.rs:verify_non_existence_proof)

The Pharos docs explicitly describe this attack: Because SkipEmpty hashing concatenates only non-empty slot hashes in scan order, an attacker can move a hash from the target slot to any empty slot without changing the node hash:

Real node:   slot[7] = H_child_k   (key K exists here)
Forged node: slot[7] = 0x00        slot[9] = H_child_k  (moved)
Both hash to: SHA256(header || H_child_a || H_child_k || H_child_c)  -- identical!

The defense requires sibling proofs for every non-empty slot. But verify_non_existence_proof only checks the siblings that are provided -- it never verifies that all non-empty slots have a corresponding sibling proof. An attacker can omit the sibling proof for the relocated slot.

Fix: After the main chain walk, count non-empty slots in the terminal node (excluding the target), assert sibling_proofs.len() == count, and verify each slot_index matches a non-empty slot.


HIGH Findings

(g) Contract totalStake proven but ignored (state_proof.rs:143)

let _total_stake = decode_u256_from_storage(&values[0])?;

The on-chain totalStake is fetched and proven against state root, but discarded. The validator set's total_stake is computed by summing individual stakes. Should cross-check.


MEDIUM Findings

| Finding | Location

-- | -- | --
(e) | BLS verifies against Header::hash() but RPC returns a separate block_proof_hash -- need to confirm which message validators actually sign | verifier/lib.rs:verify_bls_signature
(f) | Duplicate participant keys not checked before BLS aggregation -- could inflate stake | verifier/lib.rs:verify_bls_signature
(h) | Both "missing proof" and "invalid proof" return StorageProofLookupFailed | state_proof.rs:verify_all_storage_proofs
(j) | Redundant staleness check with inconsistent error messages | ismp/clients/pharos
(l) | verify_membership checks key existence but not value content | state-machines/pharos
(m) | 20-byte keys silently mapped to None without any proof | state-machines/pharos


Test Coverage Gaps

All tests are #[ignore] (require PHAROS_ATLANTIC_RPC). Missing:

  • Deterministic unit tests with hardcoded proof fixtures (for CI)
  • SkipEmpty attack test for finding (a)
  • Duplicate participant BLS test
  • Fraud proof test

Top Recommendations

  1. Fix the SkipEmpty attack (a) -- this is the most critical issue, directly exploitable for forging non-existence proofs
  2. Cross-check totalStake (g)
  3. Clarify BLS signed message (e) -- confirm header hash vs block_proof_hash
  4. Deduplicate participant keys (f) before aggregation
  5. Add CI-friendly unit tests with hardcoded proof data

@dharjeezy dharjeezy requested a review from Wizdave97 April 2, 2026 13:42
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.

Pharos Network Integration

3 participants